diff --git a/.env.example b/.env.example
deleted file mode 100644
index fb5827c..0000000
--- a/.env.example
+++ /dev/null
@@ -1,26 +0,0 @@
-# Gitea-Konfiguration für Issue-Management
-GITEA_URL=https://your-gitea-instance.com
-REPO_OWNER=your-username
-REPO_NAME=furt
-GITEA_TOKEN=your-gitea-token-here
-
-# Optional: Default-Assignee für Issues
-DEFAULT_ASSIGNEE=your-username
-
-# Gateway-Konfiguration (für Entwicklung)
-GATEWAY_PORT=8080
-GATEWAY_LOG_LEVEL=info
-
-# Service-Ports (für lokale Entwicklung)
-FORMULAR2MAIL_PORT=8081
-SAGJAN_PORT=8082
-
-# SMTP-Konfiguration (für formular2mail)
-SMTP_HOST=localhost
-SMTP_PORT=25
-SMTP_FROM=no-reply@dragons-at-work.de
-SMTP_TO=admin@dragons-at-work.de
-
-# API-Schlüssel (generiere sichere Schlüssel für Produktion!)
-HUGO_API_KEY=change-me-in-production
-ADMIN_API_KEY=change-me-in-production
diff --git a/.gitea/issue_template/architecture.yml b/.gitea/issue_template/architecture.yml
deleted file mode 100644
index da0e7e5..0000000
--- a/.gitea/issue_template/architecture.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: 🏗️ Architektur-Diskussion
-description: Diskussion über technische Entscheidungen und Architektur
-title: "[ARCH] "
-labels: ["architecture", "discussion"]
-body:
- - type: input
- id: topic
- attributes:
- label: "🎯 Thema"
- description: "Welcher Architektur-Aspekt soll diskutiert werden?"
- placeholder: "z.B. Service-Discovery, Auth-Strategy, Database-Choice"
- validations:
- required: true
-
- - type: textarea
- id: current_situation
- attributes:
- label: "📊 Aktuelle Situation"
- description: "Wie ist es momentan gelöst?"
-
- - type: textarea
- id: proposed_change
- attributes:
- label: "💡 Vorgeschlagene Änderung"
- description: "Was soll geändert/diskutiert werden?"
- validations:
- required: true
-
- - type: textarea
- id: alternatives
- attributes:
- label: "🔄 Alternativen"
- description: "Welche anderen Ansätze gibt es?"
-
- - type: checkboxes
- id: impact_areas
- attributes:
- label: "📈 Betroffene Bereiche"
- description: "Welche Teile des Systems sind betroffen?"
- options:
- - label: "Gateway-Performance"
- - label: "Service-Integration"
- - label: "Sicherheit"
- - label: "Skalierbarkeit"
- - label: "Wartbarkeit"
- - label: "Deployment"
diff --git a/.gitea/issue_template/bug_report.yml b/.gitea/issue_template/bug_report.yml
deleted file mode 100644
index 9e74f39..0000000
--- a/.gitea/issue_template/bug_report.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-name: 🐛 Bug Report
-description: Problem mit Gateway oder Service melden
-title: "[BUG] "
-labels: ["bug"]
-body:
- - type: dropdown
- id: component
- attributes:
- label: "🎯 Betroffene Komponente"
- description: "Welcher Teil des Systems ist betroffen?"
- options:
- - "Gateway (Routing, Auth, etc.)"
- - "Service: formular2mail"
- - "Service: sagjan"
- - "Konfiguration"
- - "Deployment/Scripts"
- - "Dokumentation"
- validations:
- required: true
-
- - type: textarea
- id: bug_description
- attributes:
- label: "📝 Bug-Beschreibung"
- description: "Was ist das Problem?"
- placeholder: "Detaillierte Beschreibung des Bugs"
- validations:
- required: true
-
- - type: textarea
- id: steps_to_reproduce
- attributes:
- label: "🔄 Schritte zur Reproduktion"
- description: "Wie kann der Bug reproduziert werden?"
- placeholder: |
- 1. Gehe zu ...
- 2. Klicke auf ...
- 3. Führe aus ...
- 4. Fehler tritt auf
- validations:
- required: true
-
- - type: textarea
- id: expected_behavior
- attributes:
- label: "✅ Erwartetes Verhalten"
- description: "Was sollte stattdessen passieren?"
- validations:
- required: true
diff --git a/.gitea/issue_template/service_request.yml b/.gitea/issue_template/service_request.yml
deleted file mode 100644
index 0b07b98..0000000
--- a/.gitea/issue_template/service_request.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-name: 🔧 Neuer Service für API-Gateway
-description: Anfrage für einen neuen Service im Furt-Gateway
-title: "[SERVICE] "
-labels: ["service-request", "enhancement"]
-body:
- - type: input
- id: service_name
- attributes:
- label: "🏷️ Service-Name"
- description: "Wie soll der neue Service heißen?"
- placeholder: "z.B. newsletter, shop, calendar"
- validations:
- required: true
-
- - type: textarea
- id: service_description
- attributes:
- label: "📝 Service-Beschreibung"
- description: "Was soll der Service tun?"
- placeholder: "Detaillierte Beschreibung der gewünschten Funktionalität"
- validations:
- required: true
-
- - type: input
- id: service_port
- attributes:
- label: "🔌 Gewünschter Port"
- description: "Auf welchem Port soll der Service laufen?"
- placeholder: "z.B. 8083, 8084"
-
- - type: dropdown
- id: priority
- attributes:
- label: "⚡ Priorität"
- description: "Wie dringend wird der Service benötigt?"
- options:
- - "🔥 Hoch - wird sofort benötigt"
- - "📊 Mittel - geplante Entwicklung"
- - "📝 Niedrig - nice to have"
- validations:
- required: true
-
- - type: checkboxes
- id: integration_needs
- attributes:
- label: "🔗 Integration-Anforderungen"
- description: "Welche Integrationen werden benötigt?"
- options:
- - label: "Hugo-Shortcode"
- - label: "OpenAPI-Dokumentation"
- - label: "Admin-Interface"
- - label: "E-Mail-Benachrichtigungen"
- - label: "Datenbank-Speicherung"
diff --git a/.gitignore b/.gitignore
index 9ed24a0..c67bf5b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,26 +1,32 @@
# Environment variables (NEVER commit!)
.env
+.env.local
+.env.production
+.env.development
+# Note: .env.example SHOULD be included for users
-# Go build artifacts
-*.exe
-*.exe~
-*.dll
-*.so
-*.dylib
-furt-gateway
-formular2mail-service
-sagjan-service
-/build/
-/dist/
+# Lua specific
+*.luac
+.luarocks/
+luarocks.lock
-# Go test files
-*.test
-*.out
-coverage.txt
-coverage.html
+# Furt runtime/build artifacts
+bin/
+logs/
+tmp/
+pid/
+dist/
+scripts/upload-package.sh
-# Go modules
-/vendor/
+# Issue creation scripts (these create issues, don't version them)
+scripts/gitea-issues/
+
+# Gitea internal workflow (not for end users)
+.gitea/
+
+# Gitea Tools
+tools/gitea
+issue-*.md
# OS generated files
.DS_Store
@@ -57,6 +63,9 @@ debug.log
*.sqlite3
# Configuration files with secrets
-config.local.yaml
-config.production.yaml
+config.local.lua
+config.production.lua
+config/furt.conf
+
+scripts/production_test_sequence.sh
diff --git a/.version_history b/.version_history
new file mode 100644
index 0000000..26b078b
--- /dev/null
+++ b/.version_history
@@ -0,0 +1,33 @@
+# merkwerk version history
+# Format: content_hash,vcs_hash,branch,timestamp,author,vcs_type,project_type
+7e82f537,7053af3,main,2025-08-19T18:14:06Z,michael,git,lua-api
+7e41647c,00b8a18,main,2025-08-19T19:36:33Z,michael,git,lua-api
+7e41647c,62ddc17,main,2025-08-20T04:08:04Z,michael,git,lua-api
+7e41647c,95dcdba,main,2025-08-28T15:34:36Z,michael,git,lua-api
+7ca7e6d6,8ec4019,feature/issue-89-multi-tenant,2025-08-28T17:53:42Z,michael,git,lua-api
+25a29c32,5c17c86,feature/issue-89-multi-tenant,2025-08-29T18:01:55Z,michael,git,lua-api
+25a29c32,9b19b6a,main,2025-08-29T20:01:44Z,michael,git,lua-api
+25a29c32,11ceb18,fix/service-detection,2025-09-02T16:36:07Z,michael,git,lua-api
+25a29c32,c575d5e,main,2025-09-02T19:24:58Z,michael,git,lua-api
+25a29c32,cec390e,main,2025-09-02T19:45:08Z,michael,git,lua-api
+25a29c32,0c59b27,main,2025-09-03T09:02:41Z,michael,git,lua-api
+25a29c32,589dccc,main,2025-09-03T10:16:05Z,michael,git,lua-api
+25a29c32,53ef8ad,main,2025-09-03T10:23:47Z,michael,git,lua-api
+25a29c32,eb64c39,main,2025-09-03T18:25:18Z,michael,git,lua-api
+25a29c32,38a1108,main,2025-09-03T20:13:08Z,michael,git,lua-api
+25a29c32,442b465,fix/systemd-type-forking,2025-09-05T15:02:31Z,michael,git,lua-api
+25a29c32,c15b01a,fix/config-path-consistency,2025-09-05T15:21:25Z,michael,git,lua-api
+795f8867,78e8ded,fix/json-library-compatibility,2025-09-05T15:44:42Z,michael,git,lua-api
+795f8867,d4fa6e3,fix/ssl-dependency-check,2025-09-05T16:20:08Z,michael,git,lua-api
+a670de0f,d271b84,refactor/extract-health-routes-and-server-core,2025-09-05T17:25:09Z,michael,git,lua-api
+a670de0f,25a709e,feature/pid-file-service-management,2025-09-05T20:30:13Z,michael,git,lua-api
+a670de0f,59f372f,feature/pid-file-service-management,2025-09-07T14:58:01Z,michael,git,lua-api
+a670de0f,683d6e5,fix/validate-config-posix-regex,2025-09-07T16:00:48Z,michael,git,lua-api
+a670de0f,24bd94d,feature/systemd-hardening,2025-09-07T16:40:47Z,michael,git,lua-api
+4ee95dbc,08b49d3,security/sanitize-test-scripts,2025-09-07T19:25:38Z,michael,git,lua-api
+59c85431,8b78066,main,2025-09-10T10:20:50Z,michael,git,lua-api
+a71dd794,f5d9f35,main,2025-09-10T12:27:54Z,michael,git,lua-api
+de5318f2,304b010,main,2025-09-10T14:45:12Z,michael,git,lua-api
+980d67cd,7a921dc,main,2025-09-10T14:46:13Z,michael,git,lua-api
+efbcbbd8,f20915f,main,2025-09-10T18:01:18Z,michael,git,lua-api
+f777e765,f684ea1,main,2025-09-10T18:04:19Z,michael,git,lua-api
diff --git a/LICENSE b/LICENSE
index 6d5fbb8..f2f05b5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,18 @@
-Apache License
-Version 2.0, January 2004
-http://www.apache.org/licenses/
+ISC license
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+Copyright (C) 2025 Dragons@Work
+
+Permission to use, copy, modify, and/or distribute this software
+for any purpose with or without fee is hereby granted, provided
+that the above copyright notice and this permission notice appear
+in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
+WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
+AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
+OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
-[Complete Apache 2.0 license text would go here]
diff --git a/README.md b/README.md
index 1510e48..a18d120 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,83 @@
# Furt API Gateway
-Ein Low-Tech API-Gateway für selbst-gehostete Services im Einklang mit digitaler Souveränität.
+**Pure Lua HTTP-Server für digitale Souveränität**
## Überblick
-Furt ist ein minimalistischer API-Gateway, der verschiedene Services unter einer einheitlichen API vereint. Der Name "Furt" (germanisch für "Durchgang durch Wasser") symbolisiert die Gateway-Funktion: Alle Requests durchqueren die API-Furt um zu den dahinterliegenden Services zu gelangen.
+Furt ist ein minimalistisches HTTP-Server in Lua 5.1 für Mail-Versendung über SMTP. Es bietet eine einfache JSON-API für Web-Integration und Multi-Tenant-Unterstützung über API-Keys.
-## Philosophie
+## Features
-- **Low-Tech-Ansatz**: Einfachheit vor Komplexität
-- **Digitale Souveränität**: Vollständige Kontrolle über die eigene Infrastruktur
-- **Native Deployment**: Go-Binaries ohne externe Abhängigkeiten
-- **Ressourcenschonend**: Minimaler Speicher- und CPU-Verbrauch
-- **Open Source**: Transparent und gemeinschaftlich entwickelt
+- HTTP-Server mit JSON-APIs
+- Multi-Tenant Mail-Routing über SMTP
+- API-Key-basierte Authentifizierung
+- Health-Check-Endpoints
+- Rate-Limiting pro API-Key
+- CORS-Support für Frontend-Integration
-## Status
+## Quick Start
-🚧 **In Entwicklung** - Grundgerüst wird implementiert
+**Dependencies installieren:**
+```bash
+# OpenBSD
+doas pkg_add lua lua-socket lua-cjson luasec
-## Geplante Services
+# Debian/Ubuntu
+sudo apt install lua5.1 lua-socket lua-cjson lua-sec
-- **formular2mail**: Kontaktformulare zu E-Mail weiterleiten
-- **sagjan**: Selbst-gehostetes Kommentarsystem
-- **Weitere**: Shop, Newsletter, Terminbuchung, etc.
+# Arch Linux
+sudo pacman -S lua51 lua51-socket lua51-dkjson lua51-sec
+```
-## Installation
+**Installation:**
+```bash
+git clone https://smida.dragons-at-work.de/DAW/furt.git
+cd furt
+sudo ./install.sh
+```
-*Dokumentation folgt mit erstem Release*
+**Server läuft auf:** http://127.0.0.1:7811
-## Entwicklung
+## API-Endpoints
-Siehe `devdocs/` für Entwicklungsrichtlinien und Architektur-Dokumentation.
+**Health Check:**
+```bash
+curl http://127.0.0.1:7811/health
+```
-## Lizenz
+**Mail senden:**
+```bash
+curl -X POST http://127.0.0.1:7811/v1/mail/send \
+ -H "X-API-Key: your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"name":"Test","email":"test@example.com","subject":"Test","message":"Test-Nachricht"}'
+```
+
+## Dokumentation
+
+**Installation & Konfiguration:** [Furt Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki)
+
+## Projektstruktur
+
+```
+furt/
+├── src/ # Lua-Source-Code
+├── config/ # Konfiguration
+├── scripts/ # Installation & Management
+└── deployment/ # System-Integration
+```
+
+## Integration
+
+**merkwerk:** Versionierte Furt-Deployment über [merkwerk](https://smida.dragons-at-work.de/DAW/merkwerk)
+
+## License
+
+ISC - Siehe [LICENSE](LICENSE) für Details.
+
+## Links
+
+- **Repository:** [Forgejo](https://smida.dragons-at-work.de/DAW/furt)
+- **Dokumentation:** [Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki)
+- **Projekt:** [Dragons@Work](https://dragons-at-work.de)
-Apache License 2.0 - Siehe [LICENSE](LICENSE) für Details.
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..845639e
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1.4
diff --git a/config/furt.conf.example b/config/furt.conf.example
new file mode 100644
index 0000000..28ffc8a
--- /dev/null
+++ b/config/furt.conf.example
@@ -0,0 +1,102 @@
+# furt.conf - Multi-Tenant Configuration Example
+# Dragons@Work Digital Sovereignty Project
+
+# Server configuration
+[server]
+host = 127.0.0.1
+port = 7811
+log_level = info
+log_requests = true
+client_timeout = 10
+
+# CORS configuration
+cors_allowed_origins = http://localhost:1313,http://127.0.0.1:1313,https://dragons-at-work.de,https://www.dragons-at-work.de
+
+# Security settings
+[security]
+rate_limit_api_key_max = 60
+rate_limit_ip_max = 100
+rate_limit_window = 3600
+enable_test_endpoint = false
+
+# Default SMTP settings (used when API keys don't have custom SMTP)
+[smtp_default]
+host = mail.dragons-at-work.de
+port = 465
+user = noreply@dragons-at-work.de
+password = your-smtp-password-here
+use_ssl = true
+
+# Dragons@Work Website
+[api_key "daw-frontend-key"]
+name = "Dragons@Work Website"
+permissions = mail:send
+allowed_ips = 127.0.0.1, 10.0.0.0/8, 192.168.0.0/16
+mail_to = admin@dragons-at-work.de
+mail_from = noreply@dragons-at-work.de
+mail_subject_prefix = "[DAW Contact] "
+
+# Biocodie Website (same SMTP, different recipient)
+[api_key "bio-frontend-key"]
+name = "Biocodie Website"
+permissions = mail:send
+allowed_ips = 127.0.0.1, 10.0.0.0/8
+mail_to = contact@biocodie.de
+mail_from = noreply@biocodie.de
+mail_subject_prefix = "[Biocodie] "
+
+# Verlag Website
+[api_key "verlag-frontend-key"]
+name = "Verlag Dragons@Work"
+permissions = mail:send
+allowed_ips = 127.0.0.1, 10.0.0.0/8
+mail_to = verlag@dragons-at-work.de
+mail_from = noreply@verlag.dragons-at-work.de
+mail_subject_prefix = "[Verlag] "
+
+# Customer with custom SMTP
+[api_key "kunde-x-frontend-key"]
+name = "Kunde X Website"
+permissions = mail:send
+allowed_ips = 1.2.3.4/32, 5.6.7.8/32
+mail_to = info@kunde-x.de
+mail_from = noreply@kunde-x.de
+mail_subject_prefix = "[Kunde X] "
+# Custom SMTP for this customer
+mail_smtp_host = mail.kunde-x.de
+mail_smtp_port = 587
+mail_smtp_user = noreply@kunde-x.de
+mail_smtp_pass = kunde-x-smtp-password
+mail_smtp_ssl = true
+
+# Customer with external provider (e.g., Gmail)
+[api_key "kunde-y-frontend-key"]
+name = "Kunde Y Website"
+permissions = mail:send
+allowed_ips = 9.10.11.12/32
+mail_to = support@kunde-y.com
+mail_from = website@kunde-y.com
+mail_subject_prefix = "[Kunde Y Support] "
+# Gmail SMTP example
+mail_smtp_host = smtp.gmail.com
+mail_smtp_port = 587
+mail_smtp_user = website@kunde-y.com
+mail_smtp_pass = gmail-app-password
+mail_smtp_ssl = true
+
+# Admin API key (full access for management)
+[api_key "admin-management-key"]
+name = "Admin Access"
+permissions = *, mail:send, auth:status
+allowed_ips = 127.0.0.1, 10.0.0.0/8
+mail_to = admin@dragons-at-work.de
+mail_from = furt-admin@dragons-at-work.de
+mail_subject_prefix = "[Furt Admin] "
+
+# Monitoring key (limited access)
+[api_key "monitoring-health-key"]
+name = "Monitoring Service"
+permissions = health:check
+allowed_ips = 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12
+# No mail config needed for monitoring
+
diff --git a/config/server.lua b/config/server.lua
new file mode 100644
index 0000000..7c4ccfd
--- /dev/null
+++ b/config/server.lua
@@ -0,0 +1,133 @@
+-- config/server.lua
+-- Multi-Tenant server configuration using nginx-style config parser
+-- Dragons@Work Digital Sovereignty Project
+
+local ConfigParser = require("src.config_parser")
+
+-- Load configuration from furt.conf
+local config = ConfigParser.load_config()
+
+-- Configure rate limiting from config
+local RateLimiter = require("src.rate_limiter")
+local rate_limits = {
+ api_key_max = config.security and config.security.rate_limit_api_key_max or 60,
+ ip_max = config.security and config.security.rate_limit_ip_max or 100,
+ window = config.security and config.security.rate_limit_window or 3600
+}
+RateLimiter:configure(rate_limits)
+
+-- Parse CORS origins from config or environment
+local function get_cors_origins()
+ -- 1. Try config file first
+ if config.server.cors_allowed_origins then
+ local origins = {}
+ for origin in config.server.cors_allowed_origins:gmatch("([^,]+)") do
+ table.insert(origins, origin:match("^%s*(.-)%s*$"))
+ end
+ return origins
+ end
+
+ -- 2. Try environment variable
+ local env_origins = os.getenv("CORS_ALLOWED_ORIGINS")
+ if env_origins then
+ local origins = {}
+ for origin in env_origins:gmatch("([^,]+)") do
+ table.insert(origins, origin:match("^%s*(.-)%s*$"))
+ end
+ return origins
+ end
+
+ -- 3. Development defaults
+ return {
+ "http://localhost:1313", -- Hugo dev server
+ "http://127.0.0.1:1313", -- Hugo dev server alternative
+ "http://localhost:3000", -- Common dev port
+ "http://127.0.0.1:3000" -- Common dev port alternative
+ }
+end
+
+-- Add legacy compatibility and runtime enhancements
+local server_config = {
+ -- HTTP Server settings (from [server] section)
+ host = config.server.host,
+ port = config.server.port,
+
+ -- Timeouts and limits
+ client_timeout = config.server.client_timeout or 10,
+
+ -- CORS Configuration (prioritize config file over environment)
+ cors = {
+ allowed_origins = get_cors_origins()
+ },
+
+ -- Logging
+ log_level = config.server.log_level or "info",
+ log_requests = config.server.log_requests or true,
+
+ -- Security settings
+ security = {
+ enable_test_endpoint = config.security and config.security.enable_test_endpoint or false,
+ rate_limits = rate_limits
+ },
+
+ -- API Keys (converted from nginx-style to old format for backward compatibility)
+ api_keys = config.api_keys,
+
+ -- Default SMTP config (for legacy compatibility)
+ mail = config.smtp_default,
+
+ -- Multi-tenant mail configuration function
+ get_mail_config_for_api_key = function(api_key)
+ return ConfigParser.get_mail_config_for_api_key(config, api_key)
+ end,
+
+ -- Raw config access (for advanced usage)
+ raw_config = config
+}
+
+-- Print configuration summary on load
+print("Furt Multi-Tenant Configuration Loaded:")
+print(" Server: " .. server_config.host .. ":" .. server_config.port)
+print(" Log Level: " .. server_config.log_level)
+
+-- Print CORS configuration
+print(" CORS Origins:")
+for i, origin in ipairs(server_config.cors.allowed_origins) do
+ print(" " .. i .. ": " .. origin)
+end
+
+-- Print security configuration
+print(" Test Endpoint: " .. (server_config.security.enable_test_endpoint and "enabled" or "disabled"))
+print(" Default SMTP: " .. (config.smtp_default.host or "not configured"))
+
+-- Print API key information
+local api_key_count = 0
+for key_name, key_config in pairs(config.api_keys) do
+ api_key_count = api_key_count + 1
+
+ -- Check if this API key has mail permissions
+ local has_mail_permission = false
+ if key_config.permissions then
+ for _, perm in ipairs(key_config.permissions) do
+ if perm == "mail:send" or perm == "*" then
+ has_mail_permission = true
+ break
+ end
+ end
+ end
+
+ local smtp_info = ""
+ if key_config.mail_smtp_host then
+ smtp_info = " (custom SMTP: " .. key_config.mail_smtp_host .. ")"
+ end
+
+ if has_mail_permission then
+ print(" API Key: " .. key_config.name .. " -> " .. key_config.mail_to .. smtp_info)
+ else
+ print(" API Key: " .. key_config.name .. " (no mail)" .. smtp_info)
+ end
+end
+print(" Total API Keys: " .. api_key_count)
+
+return server_config
+
diff --git a/configs/labels.registry b/configs/labels.registry
deleted file mode 100644
index 83d9cd6..0000000
--- a/configs/labels.registry
+++ /dev/null
@@ -1,58 +0,0 @@
-# Central Label Registry for Furt API Gateway Project
-# Format: name:color:context:usage_contexts
-# This file is the single source of truth for all labels
-
-# === CORE WORKFLOW LABELS ===
-service-request:7057ff:new_service:service_templates,status_updates
-enhancement:84b6eb:improvement:all_templates
-bug:d73a4a:error:bug_template,status_updates
-question:d876e3:discussion:question_template
-
-# === COMPONENT CATEGORIES ===
-gateway:0052cc:gateway_core:architecture_template,performance_template,service_templates
-performance:fbca04:optimization:performance_template,architecture_template
-architecture:d4c5f9:design:architecture_template,gateway
-security:28a745:security_review:security_template,architecture_template
-configuration:f9d71c:config_management:deployment_template,architecture_template
-
-# === SERVICE-SPECIFIC LABELS ===
-service-debug-check-final2:1d76db:service_integration:service_specific
-service-clean-test4:1d76db:service_integration:service_specific
-service-debug-test:1d76db:service_integration:service_specific
-service-formular2mail:1d76db:formular2mail:formular2mail_integration
-service-sagjan:1d76db:sagjan:sagjan_integration
-service-newsletter:ff6b6b:newsletter:newsletter_integration
-service-analytics:1d76db:service_integration:service_specific
-service-whatever-you-want:1d76db:service_integration:service_specific
-service-completely-absolut-new7:1d76db:service_integration:service_specific
-service-completely-absolut-new8:1d76db:service_integration:service_specific
-service-completely-absolut-new9:1d76db:service_integration:service_specific
-service-completely-absolut-new10:1d76db:service_integration:service_specific
-service-completely-absolut-new11:1d76db:service_integration:service_specific
-
-# === WORKFLOW STATE LABELS ===
-work-in-progress:fbca04:active:status_updates
-needs-review:0e8a16:review:status_updates
-blocked:d73a4a:blocked:status_updates
-ready-for-deployment:28a745:deploy_ready:status_updates
-
-# === INTEGRATION LABELS ===
-hugo-integration:ff7518:frontend:hugo_templates,integration
-api-contract:5319e7:api_design:api_templates,service_templates
-breaking-change:d73a4a:breaking:api_templates,architecture_template
-
-# === PRIORITY LABELS ===
-high-priority:d73a4a:urgent:all_templates
-low-priority:0e8a16:nice_to_have:all_templates
-
-# === META LABELS ===
-low-tech:6f42c1:low_tech_principle:architecture_template,performance_template,security_template
-digital-sovereignty:6f42c1:digital_sovereignty:architecture_template,performance_template,security_template
-good-first-issue:7057ff:beginner_friendly:manual_assignment
-help-wanted:159818:community_help:manual_assignment
-
-# === DEPLOYMENT LABELS ===
-deployment:ff7518:deployment:deployment_template
-testing:f9d71c:testing:testing_template,integration
-
-test-all-templates:ff0000:test:all_templates
diff --git a/deployment/linux/furt.service b/deployment/linux/furt.service
new file mode 100644
index 0000000..5dd1150
--- /dev/null
+++ b/deployment/linux/furt.service
@@ -0,0 +1,33 @@
+[Unit]
+Description=furt Multi-Tenant API Gateway (Security-Hardened)
+After=network.target
+
+[Service]
+Type=forking
+User=furt
+Group=furt
+ExecStart=/usr/local/share/furt/scripts/start.sh
+PIDFile=/var/run/furt/furt.pid
+WorkingDirectory=/usr/local/share/furt
+Restart=always
+RestartSec=5
+StandardOutput=journal
+StandardError=journal
+
+# === SECURITY HARDENING ===
+
+# Filesystem Protection
+ProtectSystem=strict
+ReadWritePaths=/var/run/furt /var/log/furt
+ProtectHome=yes
+
+# Process Hardening
+NoNewPrivileges=yes
+PrivateTmp=yes
+
+# Network Restriction
+RestrictAddressFamilies=AF_INET
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/deployment/openbsd/rc.d-furt b/deployment/openbsd/rc.d-furt
new file mode 100644
index 0000000..bcdb4b9
--- /dev/null
+++ b/deployment/openbsd/rc.d-furt
@@ -0,0 +1,54 @@
+#!/bin/ksh
+
+daemon="/usr/local/share/furt/scripts/start.sh"
+daemon_user="_furt"
+daemon_cwd="/usr/local/share/furt"
+
+. /etc/rc.d/rc.subr
+
+# PID-File location
+pidfile="/var/run/furt/furt.pid"
+
+# Custom rc_check function (PID-File based)
+rc_check() {
+ [ -f "$pidfile" ] && kill -0 $(cat "$pidfile") 2>/dev/null
+}
+
+# Custom rc_stop function (PID-File based)
+rc_stop() {
+ if [ -f "$pidfile" ]; then
+ local _pid=$(cat "$pidfile")
+ echo "Stopping furt (PID: $_pid)"
+ kill "$_pid" 2>/dev/null
+ # Wait for process to die
+ local _timeout=10
+ while [ $_timeout -gt 0 ] && kill -0 "$_pid" 2>/dev/null; do
+ sleep 1
+ _timeout=$((_timeout - 1))
+ done
+ # Force kill if still running
+ if kill -0 "$_pid" 2>/dev/null; then
+ echo "Force killing furt (PID: $_pid)"
+ kill -9 "$_pid" 2>/dev/null
+ fi
+ rm -f "$pidfile"
+ echo "furt stopped"
+ else
+ echo "furt not running (no PID-File)"
+ fi
+}
+
+# Custom rc_reload function (signal-based)
+rc_reload() {
+ if rc_check; then
+ local _pid=$(cat "$pidfile")
+ echo "Reloading furt configuration (PID: $_pid)"
+ kill -HUP "$_pid"
+ else
+ echo "furt not running"
+ return 1
+ fi
+}
+
+rc_cmd $1
+
diff --git a/devdocs/furt_development_process.md b/devdocs/furt_development_process.md
deleted file mode 100644
index 894a640..0000000
--- a/devdocs/furt_development_process.md
+++ /dev/null
@@ -1,590 +0,0 @@
-# Entwicklungsprozess für Furt API-Gateway
-
-**Erstellt:** 03.06.2025
-**Letzte Aktualisierung:** 03.06.2025
-**Version:** 1.0
-**Verantwortlich:** Claude / DAW-Team
-**Dateipfad:** devdocs/development-process.md
-
-## Zweck dieses Dokuments
-
-Dieses Dokument definiert den verbindlichen Prozess für die Entwicklung und Änderung von Code im Rahmen des Furt API-Gateway-Projekts. Es ergänzt die allgemeinen Entwicklungsrichtlinien um API-Gateway-spezifische Patterns und Multi-Service-Koordination.
-
-Es richtet sich an alle Projektbeteiligten, die am Gateway oder Services entwickeln.
-
-## Verwandte Dokumente
-
-Dieses Dokument steht im Zusammenhang mit folgenden anderen Dokumenten:
-
-- **KONZEPT.md:** Zentrale Referenz und Konzeptdokumentation, devdocs/KONZEPT.md
-- **TESTING_GUIDELINES.md:** API-Gateway-spezifische Test-Standards, devdocs/TESTING_GUIDELINES.md
-- **ARCHITECTURE.md:** Detaillierte Systemarchitektur, devdocs/ARCHITECTURE.md
-
-## Änderungshistorie
-
-| Version | Datum | Änderungen | Autor |
-|---------|-------|------------|-------|
-| 1.0 | 03.06.2025 | Initiale Version für Furt API-Gateway | Claude / DAW-Team |
-
-## 1. Grundprinzipien für API-Gateway-Entwicklung
-
-### 1.1 Service-First-Entwicklung
-
-Jede Entwicklungsaufgabe muss im Kontext des **Service-Ökosystems** betrachtet werden:
-
-- **Gateway-Änderungen** betreffen potenziell alle Services
-- **Service-Änderungen** können Gateway-Anpassungen erfordern
-- **API-Contracts** zwischen Gateway und Services sind kritisch
-- **Breaking Changes** erfordern koordinierte Rollouts
-
-### 1.2 API-Contract-Driven Development
-
-Bevor Code geschrieben wird, müssen **API-Contracts** definiert werden:
-
-- **OpenAPI-Spezifikation** für neue Endpunkte
-- **Service-Interface-Definition** für neue Services
-- **Authentication/Authorization-Requirements** für alle APIs
-- **Error-Response-Standards** konsistent halten
-
-### 1.3 Security-First-Pattern
-
-Sicherheit wird bei **jeder** Änderung mitgedacht:
-
-- **API-Key-Berechtigungen** bei neuen Endpunkten definieren
-- **Input-Validation** für alle eingehenden Requests
-- **Rate-Limiting** für neue Services konfigurieren
-- **IP-Restrictions** wo angemessen anwenden
-
-## 2. Verbindlicher Entwicklungsprozess für Furt
-
-### 2.1 Vorbereitung
-
-1. **Requirements-Analyse mit Service-Impact**
- - Welche Services sind betroffen?
- - Welche Gateway-Komponenten benötigen Änderungen?
- - Sind Breaking Changes erforderlich?
- - Welche API-Contracts müssen definiert/aktualisiert werden?
-
-2. **Explizite Anfrage nach relevanten Dateien**
- - Gateway-Dateien: `internal/gateway/`, `configs/gateway.yaml`
- - Service-Dateien: `internal/services/[service]/`, `configs/services/`
- - API-Dokumentation: `docs/api/`, OpenAPI-Specs
- - Integration-Tests: `tests/integration/`
-
-3. **Analyse der Service-Integration-Pattern**
- - Bestehende Service-Registry-Einträge
- - Routing-Patterns und Middleware-Chain
- - Authentifizierungs-Flows
- - Health-Check-Mechanismen
-
-### 2.2 Design und Planung
-
-1. **API-First-Design dokumentieren**
- - OpenAPI-Spezifikation **vor** der Implementierung schreiben
- - Request/Response-Schemas definieren
- - HTTP-Status-Codes und Error-Handling spezifizieren
- - Authentication-Requirements dokumentieren
-
-2. **Service-Integration-Strategy festlegen**
- - Wie wird der Service im Gateway registriert?
- - Welche Health-Check-URL wird verwendet?
- - Welche Timeout-Werte sind angemessen?
- - Braucht der Service Admin-UI-Integration?
-
-3. **Breaking-Change-Impact analysieren**
- - Betrifft die Änderung bestehende API-Contracts?
- - Sind koordinierte Service-Updates erforderlich?
- - Müssen Client-Integrationen (Hugo-Shortcodes) angepasst werden?
- - Ist eine API-Versionierung (v1 → v2) notwendig?
-
-4. **Configuration-Strategy bestimmen**
- - Welche neuen Config-Parameter werden benötigt?
- - Sind Environment-Variable für Secrets erforderlich?
- - Wie wird die Config zwischen Gateway und Service koordiniert?
-
-### 2.3 Implementierung
-
-1. **Multi-Component-Development-Order**
-
- **Für neue Services:**
- ```
- 1. Service-Struktur scaffolden (service-generator.sh)
- 2. Service-Logik implementieren (Standalone-Mode)
- 3. Gateway-Integration hinzufügen
- 4. Integration-Tests schreiben
- 5. Deployment-Scripts anpassen
- ```
-
- **Für Gateway-Änderungen:**
- ```
- 1. Gateway-Kern-Logik implementieren
- 2. Middleware/Auth-Anpassungen
- 3. Service-Integration testen
- 4. Health-Check-Aggregation
- 5. Admin-Interface-Updates
- ```
-
- **Für API-Änderungen:**
- ```
- 1. OpenAPI-Spec aktualisieren
- 2. Gateway-Routing anpassen
- 3. Service-Endpunkt implementieren
- 4. Input-Validation hinzufügen
- 5. Integration-Tests erweitern
- ```
-
-2. **Koordinierte Entwicklung bei Service-Updates**
- - **Gateway-kompatible Änderungen zuerst** (additive APIs)
- - **Service-Tests** mit Gateway-Integration
- - **Backward-Compatibility** während Übergangsphase
- - **Coordinated Deployment** bei Breaking Changes
-
-3. **Configuration-Management während Entwicklung**
- - **Development-Configs** in `configs/[component].dev.yaml`
- - **Environment-Variable-Mapping** dokumentieren
- - **Config-Validation** bei Service-Start implementieren
- - **Hot-Reload** für Development (wo möglich)
-
-### 2.4 Testing-Integration
-
-1. **Multi-Layer-Testing-Strategy**
- - **Unit-Tests:** Für Gateway- und Service-Komponenten isoliert
- - **Integration-Tests:** Gateway ↔ Service-Kommunikation
- - **API-Tests:** End-to-End API-Contract-Validation
- - **Load-Tests:** Gateway-Performance mit mehreren Services
-
-2. **Test-Coordination-Pattern**
- ```go
- // Beispiel: Service-Integration-Test
- func TestGatewayServiceIntegration(t *testing.T) {
- // 1. Start Test-Service
- service := startTestService(t, serviceConfig)
- defer service.Close()
-
- // 2. Configure Gateway with Test-Service
- gateway := startTestGateway(t, gatewayConfigWithService(service.URL))
- defer gateway.Close()
-
- // 3. Test Gateway → Service communication
- testServiceAPIThroughGateway(t, gateway, service)
- }
- ```
-
-3. **Breaking-Change-Test-Strategy**
- - **Backward-Compatibility-Tests** bei API-Änderungen
- - **Version-Migration-Tests** bei Breaking Changes
- - **Client-Integration-Tests** (Hugo-Shortcode-Kompatibilität)
-
-## 3. Service-spezifische Entwicklungs-Pattern
-
-### 3.1 Neue Service-Entwicklung
-
-1. **Service-Scaffolding**
- ```bash
- ./scripts/service-generator.sh newsletter
- # Erstellt komplette Service-Struktur
- ```
-
-2. **Service-Interface-Implementation**
- ```go
- // Jeder Service muss dieses Interface implementieren
- type Service interface {
- // Gateway-Integration
- HandleRequest(w http.ResponseWriter, r *http.Request)
- HealthCheck() HealthStatus
-
- // Standalone-Mode
- HandleWithAuth(w http.ResponseWriter, r *http.Request)
-
- // Lifecycle
- Start(ctx context.Context, config ServiceConfig) error
- Stop(ctx context.Context) error
-
- // Service-Metadata
- GetServiceInfo() ServiceInfo
- }
- ```
-
-3. **Service-Registration im Gateway**
- ```yaml
- # configs/gateway.yaml
- services:
- newsletter:
- enabled: true
- path_prefix: "/v1/newsletter"
- upstream: "http://127.0.0.1:8083"
- health_check: "/health"
- timeout: 15s
- auth_required: true
- rate_limit:
- requests_per_minute: 60
- ```
-
-### 3.2 Service-API-Design-Standards
-
-1. **Einheitliche Request-Patterns**
- ```go
- // Standard Request-Wrapper
- type APIRequest struct {
- RequestID string `json:"request_id,omitempty"`
- Data interface{} `json:"data"`
- Meta map[string]interface{} `json:"meta,omitempty"`
- }
-
- // Standard Response-Wrapper
- type APIResponse struct {
- Success bool `json:"success"`
- Data interface{} `json:"data,omitempty"`
- Error *APIError `json:"error,omitempty"`
- Meta map[string]interface{} `json:"meta,omitempty"`
- RequestID string `json:"request_id,omitempty"`
- }
- ```
-
-2. **Konsistente Error-Handling**
- ```go
- // Standard Error-Format
- type APIError struct {
- Code string `json:"code"`
- Message string `json:"message"`
- Details string `json:"details,omitempty"`
- }
-
- // Standard Error-Codes
- const (
- ErrInvalidInput = "INVALID_INPUT"
- ErrUnauthorized = "UNAUTHORIZED"
- ErrServiceUnavailable = "SERVICE_UNAVAILABLE"
- ErrRateLimitExceeded = "RATE_LIMIT_EXCEEDED"
- )
- ```
-
-3. **Health-Check-Standards**
- ```go
- // Standard Health-Response
- type HealthStatus struct {
- Status string `json:"status"`
- Version string `json:"version"`
- Uptime time.Duration `json:"uptime"`
- Checks map[string]string `json:"checks"`
- Timestamp time.Time `json:"timestamp"`
- }
-
- // Status-Werte
- const (
- HealthStatusHealthy = "healthy"
- HealthStatusDegraded = "degraded"
- HealthStatusUnhealthy = "unhealthy"
- )
- ```
-
-### 3.3 Gateway-Integration-Pattern
-
-1. **Service-Discovery-Integration**
- ```go
- // Gateway registriert Services automatisch
- func (g *Gateway) RegisterService(name string, config ServiceConfig) error {
- service := &ServiceProxy{
- Name: name,
- PathPrefix: config.PathPrefix,
- Upstream: config.Upstream,
- HealthCheck: config.HealthCheck,
- // ...
- }
-
- g.services[name] = service
- g.updateRouting()
- return nil
- }
- ```
-
-2. **Request-Middleware-Chain**
- ```go
- // Standard Middleware-Order für alle Services
- func (g *Gateway) buildMiddlewareChain(serviceName string) []Middleware {
- return []Middleware{
- LoggingMiddleware,
- AuthenticationMiddleware,
- RateLimitingMiddleware(serviceName),
- ValidationMiddleware,
- ProxyMiddleware(serviceName),
- }
- }
- ```
-
-## 4. Configuration-Management-Pattern
-
-### 4.1 Hierarchische Konfiguration
-
-```go
-// Config-Loading-Reihenfolge
-func LoadConfig(serviceName string) (*Config, error) {
- config := &Config{}
-
- // 1. Default-Values
- config.ApplyDefaults()
-
- // 2. Base-Config-File
- config.LoadFromFile("configs/" + serviceName + ".yaml")
-
- // 3. Environment-specific
- if env := os.Getenv("ENVIRONMENT"); env != "" {
- config.LoadFromFile("configs/" + serviceName + "." + env + ".yaml")
- }
-
- // 4. Environment-Variables
- config.LoadFromEnv()
-
- // 5. Command-Line-Flags
- config.LoadFromFlags()
-
- return config.Validate()
-}
-```
-
-### 4.2 Service-Gateway-Config-Coordination
-
-```yaml
-# Gateway-Config für Service
-services:
- formular2mail:
- config_sync: true
- config_endpoint: "/config"
- config_push_on_change: true
-
-# Service erhält Config vom Gateway
-```
-
-### 4.3 Secrets-Management
-
-```go
-// Secrets werden nie in Config-Files gespeichert
-type ServiceConfig struct {
- // Public config
- Port string `yaml:"port"`
- LogLevel string `yaml:"log_level"`
-
- // Secrets via Environment
- APIKey string `env:"SERVICE_API_KEY"`
- DBPassword string `env:"DB_PASSWORD"`
-}
-```
-
-## 5. Security-First-Development-Pattern
-
-### 5.1 Authentication-Integration
-
-```go
-// Jeder Service-Endpunkt bekommt Auth-Context
-type AuthContext struct {
- APIKey string
- Permissions []string
- ClientIP string
- UserAgent string
- RequestID string
-}
-
-func (s *Service) HandleRequest(w http.ResponseWriter, r *http.Request) {
- // Auth-Context wird vom Gateway gesetzt
- authCtx := r.Context().Value("auth").(*AuthContext)
-
- // Service-spezifische Permission-Checks
- if !authCtx.HasPermission("service:" + s.name + ":write") {
- http.Error(w, "Forbidden", http.StatusForbidden)
- return
- }
-
- // ... Service-Logik
-}
-```
-
-### 5.2 Input-Validation-Standards
-
-```go
-// Standard Validation-Middleware
-func ValidationMiddleware(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // 1. Content-Type validation
- if !isValidContentType(r.Header.Get("Content-Type")) {
- http.Error(w, "Invalid Content-Type", http.StatusBadRequest)
- return
- }
-
- // 2. Content-Length limits
- if r.ContentLength > MaxRequestSize {
- http.Error(w, "Request too large", http.StatusRequestEntityTooLarge)
- return
- }
-
- // 3. Request-Body validation (service-specific)
- if err := validateRequestBody(r); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- next.ServeHTTP(w, r)
- })
-}
-```
-
-### 5.3 Rate-Limiting-Strategy
-
-```go
-// Service-spezifische Rate-Limits
-type RateLimitConfig struct {
- RequestsPerMinute int `yaml:"requests_per_minute"`
- BurstSize int `yaml:"burst_size"`
- PerAPIKey bool `yaml:"per_api_key"`
- PerIP bool `yaml:"per_ip"`
- Whitelist []string `yaml:"whitelist"`
-}
-
-// Gateway-Level Rate-Limiting
-func (g *Gateway) GetRateLimit(serviceName, apiKey, clientIP string) *RateLimit {
- config := g.getRateLimitConfig(serviceName)
- key := buildRateLimitKey(config, apiKey, clientIP)
- return g.rateLimiter.GetLimit(key)
-}
-```
-
-## 6. Breaking-Change-Management
-
-### 6.1 API-Versionierung-Strategy
-
-```go
-// URL-basierte Versionierung
-// /v1/mail/send → formular2mail v1
-// /v2/mail/send → formular2mail v2
-
-// Gateway-Routing für mehrere Versionen
-services:
- formular2mail-v1:
- path_prefix: "/v1/mail"
- upstream: "http://127.0.0.1:8081"
- formular2mail-v2:
- path_prefix: "/v2/mail"
- upstream: "http://127.0.0.1:8084"
-```
-
-### 6.2 Backward-Compatibility-Pattern
-
-```go
-// Service unterstützt mehrere API-Versionen
-func (s *FormularService) HandleRequest(w http.ResponseWriter, r *http.Request) {
- version := extractAPIVersion(r.URL.Path) // v1, v2
-
- switch version {
- case "v1":
- s.handleV1Request(w, r)
- case "v2":
- s.handleV2Request(w, r)
- default:
- s.handleLatestRequest(w, r)
- }
-}
-```
-
-### 6.3 Migration-Pattern
-
-```go
-// Coordinated Service-Migration
-type MigrationPlan struct {
- FromVersion string
- ToVersion string
- Steps []MigrationStep
-}
-
-type MigrationStep struct {
- Name string
- Component string // "gateway" | "service"
- Action string // "deploy" | "config" | "test"
- Rollback func() error
-}
-```
-
-## 7. Checkliste für API-Gateway-Entwicklung
-
-### 7.1 Vor Implementierungsbeginn
-
-- [ ] **Service-Impact analysiert:** Welche Services sind betroffen?
-- [ ] **API-Contract definiert:** OpenAPI-Spec erstellt/aktualisiert?
-- [ ] **Gateway-Integration geplant:** Routing, Auth, Rate-Limiting?
-- [ ] **Config-Strategy festgelegt:** Neue Parameter dokumentiert?
-- [ ] **Breaking-Change-Assessment:** Versionierung erforderlich?
-- [ ] **Security-Requirements:** Auth, Validation, Rate-Limiting?
-- [ ] **Test-Strategy:** Unit, Integration, API-Tests geplant?
-
-### 7.2 Während der Implementierung
-
-- [ ] **Service-Interface-Compliance:** Standard-Interface implementiert?
-- [ ] **Error-Handling-Consistency:** Standard-Error-Format verwendet?
-- [ ] **Health-Check-Integration:** Standardisierte Health-Endpoint?
-- [ ] **Logging-Standards:** Strukturierte Logs mit Request-IDs?
-- [ ] **Config-Validation:** Startup-Config-Checks implementiert?
-- [ ] **Auth-Integration:** Gateway-Auth-Context respektiert?
-- [ ] **Documentation-Update:** API-Docs und Service-Docs aktualisiert?
-
-### 7.3 Nach Implementierungsabschluss
-
-- [ ] **Integration-Tests:** Gateway ↔ Service-Tests bestehen?
-- [ ] **API-Contract-Tests:** OpenAPI-Compliance validiert?
-- [ ] **Performance-Tests:** Load-Tests mit Gateway durchgeführt?
-- [ ] **Security-Tests:** Auth, Input-Validation, Rate-Limiting getestet?
-- [ ] **Deployment-Scripts:** Service-Deployment automatisiert?
-- [ ] **Monitoring-Integration:** Health-Checks und Metriken?
-- [ ] **Documentation-Complete:** Service-Integration dokumentiert?
-
-## 8. Troubleshooting-Pattern
-
-### 8.1 Service-Integration-Debugging
-
-```go
-// Standard Debug-Endpoints für Services
-func (s *Service) RegisterDebugEndpoints() {
- http.HandleFunc("/debug/config", s.debugConfig)
- http.HandleFunc("/debug/health-detail", s.debugHealthDetail)
- http.HandleFunc("/debug/metrics", s.debugMetrics)
- http.HandleFunc("/debug/auth", s.debugAuth)
-}
-
-// Gateway Debug-Endpoints
-func (g *Gateway) RegisterDebugEndpoints() {
- http.HandleFunc("/debug/services", g.debugServices)
- http.HandleFunc("/debug/routing", g.debugRouting)
- http.HandleFunc("/debug/auth-keys", g.debugAuthKeys)
-}
-```
-
-### 8.2 Request-Tracing
-
-```go
-// Request-ID-Propagation durch alle Services
-func (g *Gateway) addRequestID(r *http.Request) *http.Request {
- requestID := r.Header.Get("X-Request-ID")
- if requestID == "" {
- requestID = generateRequestID()
- }
-
- // Für Service-Weiterleitung
- r.Header.Set("X-Request-ID", requestID)
-
- // Für Logging
- ctx := context.WithValue(r.Context(), "request_id", requestID)
- return r.WithContext(ctx)
-}
-```
-
-## 9. Zusammenfassung: API-Gateway-Entwicklungs-Goldene-Regeln
-
-1. **API-Contract-First:** OpenAPI-Spec vor Code-Implementation
-2. **Service-Integration-Aware:** Jede Änderung auf Service-Impact prüfen
-3. **Security-by-Default:** Auth, Validation, Rate-Limiting bei jedem Endpunkt
-4. **Configuration-Hierarchie:** Defaults → Environment → Service-specific
-5. **Multi-Layer-Testing:** Unit → Integration → API → E2E
-6. **Breaking-Change-Coordination:** Versionierung und Migration planen
-7. **Health-Check-Integration:** Jeder Service braucht standardisierten Health-Endpoint
-8. **Request-Tracing:** Request-IDs durch gesamte Pipeline propagieren
-
----
-
-**Wichtiger Hinweis:** Diese Entwicklungs-Pattern sind spezifisch für das Furt API-Gateway-System optimiert und sollten bei jeder Entwicklungsaufgabe konsultiert werden, um konsistente und gut integrierte Services zu gewährleisten.
\ No newline at end of file
diff --git a/devdocs/furt_konzept.md b/devdocs/furt_konzept.md
deleted file mode 100644
index e7c7605..0000000
--- a/devdocs/furt_konzept.md
+++ /dev/null
@@ -1,520 +0,0 @@
-# Furt: API-Gateway im Einklang mit digitaler Souveränität
-
-**Erstellt:** 03. Juni 2025
-**Letzte Aktualisierung:** 03. Juni 2025
-**Version:** 1.0
-**Verantwortlich:** DAW-Team
-**Dateipfad:** devdocs/KONZEPT.md
-
-## Zweck dieses Dokuments
-
-Dieses Dokument definiert die grundlegenden Prinzipien, technischen Entscheidungen und Entwicklungsrichtlinien für das Furt API-Gateway-System. Es dient als zentrale Referenz für alle Entwickler und Mitwirkenden des Projekts.
-
-Es richtet sich an Entwickler, Projektbeteiligte und alle, die am Code-Design und der Implementierung von Furt arbeiten.
-
-## Verwandte Dokumente
-
-Dieses Dokument steht im Zusammenhang mit folgenden anderen Dokumenten:
-
-- **README.md:** Öffentliche Projektbeschreibung, ../README.md
-- **ARCHITECTURE.md:** Detaillierte Architekturübersicht, devdocs/ARCHITECTURE.md
-- **DECISIONS.md:** Wichtige Architekturentscheidungen, devdocs/DECISIONS.md
-
-## 1. Projektvision und Philosophie
-
-Furt (germanisch für "Durchgang durch Wasser") ist ein selbst-gehostetes API-Gateway-System, das vollständig im Einklang mit den Prinzipien digitaler Souveränität, technologischer Angemessenheit und ressourcenschonender Entwicklung steht. Das System soll:
-
-- **Einfachheit über Komplexität stellen** - leicht verständlich, wartbar und erweiterbar sein
-- **Ressourceneffizient arbeiten** - minimale Server-Anforderungen und optimierte Performance
-- **Vollständige Kontrolle** ermöglichen - transparenter Code ohne Black-Boxes
-- **Langfristig tragfähig** sein - basierend auf bewährten Technologien
-- **Service-Modularität** unterstützen - eigenständige Services unter einheitlicher API
-- **Mehrsprachigkeit** von Anfang an unterstützen (DE, EN, FR)
-
-Im Gegensatz zu existierenden Enterprise-Gateway-Lösungen fokussiert sich Furt auf:
-- Native Installation als Hauptdeployment-Methode (keine Container-Abhängigkeit)
-- Minimale externe Abhängigkeiten
-- Transparente Konfiguration und Datenverarbeitung
-- Volle Kompatibilität mit Low-Tech-Webseiten und statischen Site-Generatoren (besonders Hugo)
-- **Ein Gateway für alle Services** - von Kontaktformularen bis zu komplexeren Anwendungen
-
-## 2. Technische Architektur
-
-### 2.1 Technology-Stack
-
-- **Backend:** Go (für Performance, einfache Deployment durch einzelne Binärdatei)
-- **Gateway-Pattern:** Reverse Proxy mit Service Registry
-- **Konfiguration:** YAML-basiert (human-readable, versionierbar)
-- **Proxy-Integration:** Apache als SSL-terminierender Reverse Proxy
-- **Services:** Eigenständige Go-Binaries mit eigenen Ports
-- **Authentifizierung:** API-Keys mit granularen Berechtigungen
-- **Logging:** Strukturierte JSON-Logs mit konfigurierbaren Leveln
-
-### 2.2 Projektstruktur
-
-```
-furt/
-├── cmd/
-│ ├── furt-gateway/ # Gateway-Binary
-│ │ └── main.go
-│ └── services/ # Service-Binaries
-│ ├── formular2mail/ # Kontaktformular → E-Mail
-│ ├── sagjan/ # Kommentarsystem-Integration
-│ └── shop/ # Zukünftig: E-Commerce
-├── internal/
-│ ├── gateway/ # Gateway-Kernlogik
-│ │ ├── server.go # HTTP-Server
-│ │ ├── router.go # Request-Routing
-│ │ ├── proxy.go # Service-Proxy
-│ │ ├── auth.go # Authentifizierung
-│ │ └── middleware.go # Gateway-Middleware
-│ ├── services/ # Service-Implementierungen
-│ │ ├── formular2mail/ # Formular-Service-Logik
-│ │ └── sagjan/ # Kommentar-Service-Logik
-│ └── shared/ # Geteilte Komponenten
-│ ├── auth/ # Authentifizierungs-Bibliothek
-│ ├── config/ # Konfigurationsmanagement
-│ └── logging/ # Strukturiertes Logging
-├── configs/ # Konfigurationsvorlagen
-│ ├── gateway.yaml.example # Gateway-Konfiguration
-│ └── services/ # Service-spezifische Configs
-│ ├── formular2mail.yaml.example
-│ └── sagjan.yaml.example
-├── docs/ # Öffentliche Dokumentation
-│ ├── installation.md # Installationsanleitung
-│ ├── configuration.md # Konfigurationsreferenz
-│ ├── services/ # Service-spezifische Dokumentation
-│ └── api/ # OpenAPI-Dokumentation
-│ ├── gateway.yaml # Gateway-API-Spec
-│ └── services/ # Service-API-Specs
-├── devdocs/ # Entwicklerdokumentation
-├── scripts/ # Build & Deployment
-│ ├── build-all.sh # Alle Komponenten bauen
-│ ├── deploy-gateway.sh # Gateway deployment
-│ ├── deploy-service.sh # Service deployment
-│ └── service-generator.sh # Neuen Service scaffolden
-├── examples/ # Beispiel-Integrationen
-│ ├── hugo/ # Hugo-Shortcodes
-│ ├── nginx/ # Nginx-Proxy-Config
-│ └── apache/ # Apache-Proxy-Config
-└── tests/ # Test-Suites
- ├── integration/ # Service-Integration-Tests
- └── e2e/ # End-to-End-Tests
-```
-
-### 2.3 Gateway-Architektur
-
-Das Furt-Gateway implementiert ein **Service-Registry-Pattern** mit dateibasierter Konfiguration:
-
-```go
-// Beispiel Gateway-Konfiguration
-type GatewayConfig struct {
- Gateway GatewaySettings `yaml:"gateway"`
- Security SecurityConfig `yaml:"security"`
- Services map[string]ServiceConfig `yaml:"services"`
- Logging LoggingConfig `yaml:"logging"`
-}
-
-type ServiceConfig struct {
- Enabled bool `yaml:"enabled"`
- PathPrefix string `yaml:"path_prefix"` // /v1/mail, /v1/comments
- Upstream string `yaml:"upstream"` // http://127.0.0.1:8081
- HealthCheck string `yaml:"health_check"` // /health
- Timeout time.Duration `yaml:"timeout"`
- HasAdminUI bool `yaml:"has_admin_ui"`
- AdminPath string `yaml:"admin_path"`
-}
-```
-
-**Request-Flow:**
-1. Client → Apache (SSL-Terminierung) → Gateway
-2. Gateway → Authentifizierung (API-Key + IP-Check)
-3. Gateway → Service-Registry (Route Resolution)
-4. Gateway → Service-Proxy (Request Forwarding)
-5. Service → Response → Gateway → Client
-
-## 3. Entwicklungsphasen gemäß natürlichem Wachstum
-
-Die Entwicklung folgt einem natürlichen, organischen Prozess, der in vier Hauptphasen gegliedert ist:
-
-### 3.1 Wurzelphase (Grundlagen)
-
-- **Ziel:** Funktionierendes Gateway mit minimalen Features
-- **Schlüsselfeatures:**
- - HTTP-Gateway mit grundlegendem Routing
- - API-Key-Authentifizierung mit IP-Beschränkungen
- - Formular2Mail-Service (E-Mail-Weiterleitung)
- - Basis-Hugo-Integration (Shortcodes)
- - Native Installationspakete für alle Komponenten
-
-### 3.2 Wachstumsphase (Erweiterung)
-
-- **Ziel:** Stabile, nutzbare Version mit wichtigen Services
-- **Schlüsselfeatures:**
- - Sagjan-Integration (Kommentarsystem)
- - Erweiterte Middleware (Rate-Limiting, Logging)
- - Service-Generator für neue Services
- - Admin-Dashboard für Gateway-Management
- - Umfassende OpenAPI-Dokumentation
-
-### 3.3 Blütephase (Vernetzung)
-
-- **Ziel:** Verbesserung der Integration und Erweiterbarkeit
-- **Schlüsselfeatures:**
- - Shop-Service (E-Commerce-Integration)
- - Webhook-Support für Service-Events
- - Erweiterte Monitoring-Funktionen
- - Multi-Tenant-Fähigkeiten
- - Service-Discovery-Verbesserungen
-
-### 3.4 Fruchtphase (Reifung)
-
-- **Ziel:** Langfristige Wartbarkeit und Community-Entwicklung
-- **Schlüsselfeatures:**
- - Community-Service-Ecosystem
- - Performance-Optimierungen
- - High-Availability-Features
- - Föderation mit anderen Furt-Instanzen
-
-## 4. Service-Integration-Konzept
-
-### 4.1 Service-Entwicklung-Pattern
-
-Jeder Service folgt einem standardisierten Muster:
-
-```go
-// Service-Interface
-type Service interface {
- // Für Gateway-Integration
- HandleRequest(w http.ResponseWriter, r *http.Request)
- HealthCheck() error
-
- // Für Standalone-Mode
- HandleWithAuth(w http.ResponseWriter, r *http.Request) // Eigene Auth
-
- // Service-Lifecycle
- Start(ctx context.Context) error
- Stop(ctx context.Context) error
-}
-
-// Service-Konfiguration
-type ServiceConfig struct {
- Port string `yaml:"port"`
- Environment string `yaml:"environment"`
- Auth ServiceAuth `yaml:"auth"`
- Logging LoggingConfig `yaml:"logging"`
- Custom map[string]string `yaml:"custom"` // Service-spezifisch
-}
-```
-
-### 4.2 Dual-Mode-Operation
-
-Jeder Service kann sowohl **hinter dem Gateway** als auch **standalone** betrieben werden:
-
-**Gateway-Mode:**
-- Service läuft auf localhost:PORT
-- Authentifizierung erfolgt im Gateway
-- Routing über Gateway-Pfade (/v1/mail, /v1/comments)
-
-**Standalone-Mode:**
-- Service läuft mit eigener Authentifizierung
-- Direkte API-Endpunkte (/api/v1/mail/send)
-- Eigene Dokumentation und Admin-UI
-
-### 4.3 Service-Generator
-
-Für schnelle Service-Entwicklung existiert ein Generator:
-
-```bash
-./scripts/service-generator.sh newsletter
-# Erstellt:
-# - cmd/services/newsletter/main.go
-# - internal/services/newsletter/service.go
-# - configs/services/newsletter.yaml
-# - docs/services/newsletter.yaml
-# - Deployment-Scripts
-```
-
-## 5. Implementierungsrichtlinien
-
-### 5.1 Go-spezifische Standards
-
-- **Formatierung:** `gofmt` für alle Go-Dateien verwenden
-- **Linting:** `golangci-lint` mit angepasster Konfiguration
-- **Tests:** Für jedes Package Testdateien (coverage-Ziel: mind. 80%)
-- **Abhängigkeiten:** Minimale externe Abhängigkeiten, nur Go-Standard-Library + etablierte Packages
-- **Fehlerbehandlung:** Explizite Fehlerprüfung, strukturierte Fehlerrückgaben
-- **Logging:** Strukturiertes JSON-Logging mit verschiedenen Leveln
-- **Kommentare:** Alle Funktionen und Typen auf Englisch dokumentieren
-
-### 5.2 API-Design-Standards
-
-- **RESTful-Design:** Standard HTTP-Methoden und Status-Codes
-- **Versionierung:** `/v1/` Pfad-Prefix für alle APIs
-- **JSON-Format:** Einheitliche Request/Response-Strukturen
-- **Fehlerbehandlung:** Konsistente Fehler-Response-Formate
-- **OpenAPI:** Jeder Endpunkt wird dokumentiert
-
-### 5.3 Konfigurationsmanagement
-
-- **YAML-Format:** Human-readable, versionierbar
-- **Umgebungsvariablen:** Für sensitive Daten (Tokens, Passwörter)
-- **Hierarchische Konfiguration:** Default → Environment → Local
-- **Validierung:** Konfiguration wird beim Start validiert
-
-## 6. Qualitätssicherung
-
-### 6.1 Teststrategie
-
-- **Unit-Tests:** Für alle Kernfunktionen und Services
-- **Integration-Tests:** Für Gateway ↔ Service-Kommunikation
-- **API-Tests:** Für alle Endpunkte mit verschiedenen Authentifizierungsszenarien
-- **End-to-End-Tests:** Für vollständige User-Journeys (Hugo → Gateway → Service)
-- **Performance-Tests:** Load-Testing für Gateway und Services
-
-### 6.2 Code-Review-Prozess
-
-- Peer-Reviews für alle Änderungen
-- Prüfung auf Einhaltung der Low-Tech-Prinzipien
-- Überprüfung der API-Dokumentation
-- Fokus auf Sicherheit und Performance
-- Beachtung der Service-Integration-Patterns
-
-### 6.3 Sicherheits-Standards
-
-- **API-Key-Management:** Sichere Generation und Rotation
-- **Input-Validierung:** Alle User-Inputs validieren
-- **Rate-Limiting:** Schutz vor Abuse
-- **IP-Allowlisting:** Restriktive Service-Zugriffe
-- **Secure Headers:** Standard-Security-Headers
-- **Audit-Logging:** Sicherheitsrelevante Events loggen
-
-## 7. Deployment und Installation
-
-### 7.1 Native Installation (primär)
-
-**Gateway-Deployment:**
-```bash
-# Build
-./scripts/build-all.sh
-
-# Deploy Gateway
-./scripts/deploy-gateway.sh
-
-# Deploy Services
-./scripts/deploy-service.sh formular2mail
-./scripts/deploy-service.sh sagjan
-```
-
-**Systemd-Integration:**
-- Gateway als `furt-gateway.service`
-- Services als `formular2mail-service.service`, etc.
-- Automatischer Start und Restart
-- Structured Logging zu journald
-
-### 7.2 Apache-Integration
-
-**Reverse-Proxy-Konfiguration:**
-```apache
-
- ServerName api.dragons-at-work.de
-
- # SSL-Terminierung durch Apache
- SSLEngine on
- SSLCertificateFile /path/to/cert.pem
- SSLCertificateKeyFile /path/to/key.pem
-
- # Gateway-Proxy
- ProxyPreserveHost On
- ProxyPass / http://127.0.0.1:8080/
- ProxyPassReverse / http://127.0.0.1:8080/
-
- # Headers für Gateway
- ProxyPassReverse / http://127.0.0.1:8080/
- ProxySetHeader X-Forwarded-For %{REMOTE_ADDR}s
- ProxySetHeader X-Forwarded-Proto https
-
-```
-
-### 7.3 Monitoring und Logging
-
-**Log-Struktur:**
-```
-/var/log/furt/
-├── gateway.log # Gateway-Logs
-├── formular2mail.log # Service-Logs
-├── sagjan.log
-└── access.log # Request-Logs
-```
-
-**Health-Checks:**
-- Gateway: `/health` (Service-Status-Aggregation)
-- Services: `/health` (Individual Service Health)
-- Systemd: `watchdog` für automatischen Restart
-
-## 8. Hugo-Integration
-
-### 8.1 Shortcode-Integration
-
-**Kontaktformular:**
-```hugo
-{{< furt-contact
- form-id="contact-main"
- api-key="hugo-frontend-key"
- success-message="Vielen Dank für deine Nachricht!"
->}}
-```
-
-**Kommentarsystem:**
-```hugo
-{{< furt-comments
- page-url="{{ .Permalink }}"
- api-key="sagjan-public-key"
- moderation="true"
->}}
-```
-
-### 8.2 JavaScript-Client
-
-**Minimaler, Framework-freier JavaScript-Client:**
-```javascript
-// pkg/client/furt-client.js
-class FurtClient {
- constructor(baseURL, apiKey) {
- this.baseURL = baseURL;
- this.apiKey = apiKey;
- }
-
- async submitForm(formData, endpoint) {
- // Progressive Enhancement
- // Funktioniert mit und ohne JavaScript
- }
-}
-```
-
-## 9. Git-Workflow und Versionierung
-
-### 9.1 Branch-Strategie
-
-- `main`: Stabile Releases
-- `develop`: Hauptentwicklungszweig
-- `feature/*`: Feature-Branches
-- `service/*`: Service-spezifische Entwicklung
-- `release/*`: Release-Kandidaten
-
-### 9.2 Commit-Message-Format
-
-```
-typ(bereich): short description of the change
-
-Detailed explanation of the change, if necessary.
-Multiple lines possible.
-
-Resolves: #123
-```
-
-- **Typen:** feat, fix, docs, style, refactor, test, chore
-- **Bereiche:** gateway, service-*, config, docs, scripts
-- **Sprache:** Englisch für alle Commit-Messages
-
-### 9.3 Versionierung
-
-Semantic Versioning (MAJOR.MINOR.PATCH):
-- **MAJOR:** Breaking Changes in Gateway-API oder Service-Interfaces
-- **MINOR:** Neue Services oder Features (abwärtskompatibel)
-- **PATCH:** Bugfixes und Performance-Verbesserungen
-
-## 10. Service-Spezifikationen
-
-### 10.1 Formular2Mail-Service
-
-**Zweck:** Kontaktformulare zu E-Mail weiterleiten
-**Port:** 8081
-**API:** `/send` (POST)
-**Integration:** SMTP mit bestehendem Postfix
-**Hugo-Integration:** Shortcode für Kontaktformulare
-
-### 10.2 Sagjan-Service (geplant)
-
-**Zweck:** Kommentarsystem-Integration
-**Port:** 8082
-**API:** `/comments/*` (GET, POST, PUT, DELETE)
-**Features:** Moderation, Threading, Spam-Schutz
-**Hugo-Integration:** Shortcode für Kommentare
-
-### 10.3 Zukünftige Services
-
-- **Shop-Service:** E-Commerce-Funktionen
-- **Newsletter-Service:** Listmonk-Integration
-- **Calendar-Service:** Terminbuchungen
-- **Auth-Service:** User-Management
-
-## 11. Lizenzierung
-
-### 11.1 Apache License 2.0
-
-Das Projekt steht unter der Apache License 2.0, die:
-- Kommerzielle Nutzung erlaubt
-- Patentrechte explizit gewährt
-- Modifikationen ermöglicht
-- Verteilung gestattet
-
-### 11.2 Copyright-Header
-
-Jede Quellcode-Datei muss folgenden Header enthalten:
-
-```go
-// Copyright 2025 Furt Contributors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-```
-
-## 12. Nächste Schritte
-
-### 12.1 Unmittelbare Implementierung (Wurzelphase)
-
-1. **Gateway-Grundgerüst entwickeln**
- - HTTP-Server mit grundlegenden Routen
- - Konfigurationsmanagement (YAML-basiert)
- - Service-Registry-Implementation
-
-2. **Formular2Mail-Service implementieren**
- - SMTP-Integration mit bestehendem Postfix
- - Input-Validierung und Fehlerbehandlung
- - Hugo-Shortcode für Kontaktformulare
-
-3. **Apache-Integration konfigurieren**
- - Reverse-Proxy für `api.dragons-at-work.de`
- - SSL-Terminierung und Header-Forwarding
- - Health-Check-Integration
-
-### 12.2 Mittelfristige Entwicklung (Wachstumsphase)
-
-1. **Authentifizierungs-System ausbauen**
- - Granulare API-Key-Berechtigungen
- - Rate-Limiting und IP-Restrictions
- - Admin-Interface für Key-Management
-
-2. **Monitoring und Logging**
- - Strukturiertes JSON-Logging
- - Health-Check-Aggregation
- - Performance-Metriken
-
-3. **Service-Generator implementieren**
- - Scaffolding für neue Services
- - Template-basierte Code-Generierung
- - Deployment-Script-Integration
-
----
-
-Diese Konzeptdokumentation dient als Leitfaden für die Entwicklung von Furt und soll im Laufe des Projekts entsprechend der Erkenntnisse aus der praktischen Umsetzung angepasst und erweitert werden. Sie bildet die Grundlage für alle weiteren Architektur- und Implementierungsentscheidungen im Projekt.
\ No newline at end of file
diff --git a/devdocs/furt_testing_guidelines.md b/devdocs/furt_testing_guidelines.md
deleted file mode 100644
index 628ac9a..0000000
--- a/devdocs/furt_testing_guidelines.md
+++ /dev/null
@@ -1,783 +0,0 @@
-# Furt Testing-Richtlinien
-
-**Erstellt:** 03.06.2025
-**Letzte Aktualisierung:** 03.06.2025
-**Version:** 1.0
-**Verantwortlich:** DAW-Team
-**Dateipfad:** devdocs/TESTING_GUIDELINES.md
-
-## Zweck dieses Dokuments
-
-Dieses Dokument definiert verbindliche Standards und Richtlinien für das Testen von Komponenten des Furt API-Gateway-Projekts. Es soll sicherstellen, dass alle implementierten Funktionalitäten ausreichend durch Tests abgedeckt sind und die Tests konsistent und wartbar bleiben.
-
-Es richtet sich an alle Entwickler, die Code zum Projekt beisteuern.
-
-## 1. Grundprinzipien
-
-### 1.1 Test-First-Entwicklung
-
-- Tests sollten parallel zur Implementierung oder idealerweise vor der eigentlichen Implementierung geschrieben werden.
-- Keine Implementierung gilt als abgeschlossen, bis entsprechende Tests vorhanden sind.
-- Pull Requests ohne Tests werden in der Regel nicht akzeptiert.
-
-### 1.2 Testabdeckung
-
-- Angestrebte Testabdeckung für Gateway-Kern: mindestens 85%
-- Angestrebte Testabdeckung für Services: mindestens 80%
-- Angestrebte Testabdeckung für Shared-Libraries: mindestens 90%
-- Besonders kritische Komponenten (Authentifizierung, Routing, Service-Proxy) sollten eine Abdeckung nahe 100% haben.
-
-### 1.3 Test-Typen
-
-Folgende Test-Typen werden im Projekt verwendet:
-
-1. **Unit Tests**: Testen einzelner Funktionen/Methoden in Isolation
-2. **Integration Tests**: Testen des Zusammenspiels von Komponenten
-3. **API Tests**: Testen der API-Endpunkte des Gateways und Services
-4. **Service Integration Tests**: Testen der Gateway ↔ Service-Kommunikation
-5. **End-to-End Tests**: Testen der gesamten Request-Pipeline (Client → Gateway → Service)
-6. **Performance Tests**: Load-Testing für Gateway und Services
-
-## 2. Test-Struktur und Dateiorganisation
-
-### 2.1 Dateistruktur
-
-- Test-Dateien werden neben den zu testenden Dateien platziert und erhalten den Suffix `_test.go`
-- Beispiel: `gateway.go` → `gateway_test.go`
-- Integration-Tests werden in `tests/integration/` platziert
-- End-to-End-Tests werden in `tests/e2e/` platziert
-
-### 2.2 Namenkonventionen
-
-- Testfunktionen folgen dem Format `Test`
-- Beispiel: `TestGatewayRoutingWithValidAPIKey`, `TestServiceProxyWhenServiceUnavailable`
-- Benchmark-Tests: `Benchmark`
-- Example-Tests: `Example`
-
-### 2.3 Testpakete
-
-- Tests sollten im selben Paket wie der zu testende Code sein (kein separates `_test`-Paket)
-- Dies ermöglicht das Testen von Funktionen, die nicht exportiert werden
-- Ausnahme: Integration-Tests können separate Pakete verwenden
-
-## 3. Unit Tests
-
-### 3.1 Grundstruktur
-
-Jeder Unit Test sollte folgende Struktur haben:
-
-```go
-func TestFunctionName(t *testing.T) {
- // Arrange: Vorbereitung der Testdaten und Abhängigkeiten
- input := setupTestInput()
- mockService := &MockService{}
- expected := expectedResult{}
-
- // Act: Ausführen der zu testenden Funktion
- actual, err := FunctionName(input, mockService)
-
- // Assert: Überprüfung des Ergebnisses
- if err != nil {
- t.Fatalf("Expected no error, but got: %v", err)
- }
-
- if !reflect.DeepEqual(actual, expected) {
- t.Errorf("Expected %+v, but got %+v", expected, actual)
- }
-}
-```
-
-### 3.2 Table-Driven Tests
-
-Für komplexere Funktionen sollten Table-Driven Tests verwendet werden:
-
-```go
-func TestGatewayRouting(t *testing.T) {
- testCases := []struct {
- name string
- requestPath string
- expectedService string
- expectedPath string
- wantErr bool
- }{
- {
- name: "formular2mail service routing",
- requestPath: "/v1/mail/send",
- expectedService: "formular2mail",
- expectedPath: "/send",
- wantErr: false,
- },
- {
- name: "sagjan service routing",
- requestPath: "/v1/comments/list",
- expectedService: "sagjan",
- expectedPath: "/list",
- wantErr: false,
- },
- {
- name: "unknown service",
- requestPath: "/v1/unknown/test",
- expectedService: "",
- expectedPath: "",
- wantErr: true,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- gateway := setupTestGateway()
- service, path, err := gateway.ResolveRoute(tc.requestPath)
-
- if tc.wantErr && err == nil {
- t.Error("Expected error, but got nil")
- }
- if !tc.wantErr && err != nil {
- t.Fatalf("Expected no error, but got: %v", err)
- }
-
- if service != tc.expectedService {
- t.Errorf("Expected service %s, got %s", tc.expectedService, service)
- }
- if path != tc.expectedPath {
- t.Errorf("Expected path %s, got %s", tc.expectedPath, path)
- }
- })
- }
-}
-```
-
-### 3.3 Mocking und Test-Doubles
-
-- Für HTTP-Clients verwende `httptest.NewServer`
-- Für Services erstelle Interface-basierte Mocks
-- Verwende Dependency Injection für bessere Testbarkeit
-
-```go
-// Service Interface für Testbarkeit
-type MailService interface {
- SendMail(request MailRequest) error
-}
-
-// Mock Implementation
-type MockMailService struct {
- SendMailFunc func(MailRequest) error
- CallCount int
-}
-
-func (m *MockMailService) SendMail(request MailRequest) error {
- m.CallCount++
- if m.SendMailFunc != nil {
- return m.SendMailFunc(request)
- }
- return nil
-}
-
-// Test mit Mock
-func TestFormular2MailHandler(t *testing.T) {
- mockService := &MockMailService{
- SendMailFunc: func(req MailRequest) error {
- if req.Email == "invalid" {
- return errors.New("invalid email")
- }
- return nil
- },
- }
-
- handler := NewFormular2MailHandler(mockService)
-
- // Test ausführen...
-}
-```
-
-## 4. Integration Tests
-
-### 4.1 Gateway-Service Integration Tests
-
-Diese Tests prüfen die Kommunikation zwischen Gateway und Services:
-
-```go
-// tests/integration/gateway_service_test.go
-func TestGatewayServiceIntegration(t *testing.T) {
- // Setup Test-Services
- mailService := startTestMailService(t)
- defer mailService.Close()
-
- commentsService := startTestCommentsService(t)
- defer commentsService.Close()
-
- // Setup Gateway mit Test-Konfiguration
- gateway := setupTestGateway(t, GatewayConfig{
- Services: map[string]ServiceConfig{
- "formular2mail": {
- Enabled: true,
- PathPrefix: "/v1/mail",
- Upstream: mailService.URL,
- },
- "sagjan": {
- Enabled: true,
- PathPrefix: "/v1/comments",
- Upstream: commentsService.URL,
- },
- },
- })
- defer gateway.Close()
-
- // Test Gateway → Service Routing
- t.Run("mail service integration", func(t *testing.T) {
- resp := makeTestRequest(t, gateway.URL+"/v1/mail/send", "POST", mailRequestBody)
- assertStatusCode(t, resp, http.StatusOK)
- })
-
- t.Run("comments service integration", func(t *testing.T) {
- resp := makeTestRequest(t, gateway.URL+"/v1/comments", "GET", nil)
- assertStatusCode(t, resp, http.StatusOK)
- })
-}
-```
-
-### 4.2 Database Integration Tests (für Services)
-
-Für Services mit Datenbank-Zugriff:
-
-```go
-func TestSagjanServiceDatabaseIntegration(t *testing.T) {
- // Setup Test-Database (SQLite in-memory)
- db := setupTestDB(t)
- defer db.Close()
-
- service := NewSagjanService(db)
-
- // Test Comment Creation
- comment := &Comment{
- PageURL: "https://example.com/test",
- Author: "Test User",
- Content: "Test Comment",
- }
-
- err := service.CreateComment(context.Background(), comment)
- if err != nil {
- t.Fatalf("Failed to create comment: %v", err)
- }
-
- // Test Comment Retrieval
- comments, err := service.GetComments(context.Background(), "https://example.com/test")
- if err != nil {
- t.Fatalf("Failed to get comments: %v", err)
- }
-
- if len(comments) != 1 {
- t.Errorf("Expected 1 comment, got %d", len(comments))
- }
-}
-
-func setupTestDB(t *testing.T) *sql.DB {
- db, err := sql.Open("sqlite3", ":memory:")
- if err != nil {
- t.Fatalf("Failed to open test database: %v", err)
- }
-
- // Run migrations
- if err := runMigrations(db); err != nil {
- t.Fatalf("Failed to run migrations: %v", err)
- }
-
- return db
-}
-```
-
-## 5. API Tests
-
-### 5.1 Gateway API Tests
-
-Tests für die Gateway-API-Endpunkte:
-
-```go
-func TestGatewayAPIEndpoints(t *testing.T) {
- gateway := setupTestGateway(t)
- defer gateway.Close()
-
- testCases := []struct {
- name string
- method string
- path string
- headers map[string]string
- body string
- expectedStatus int
- expectedBody string
- }{
- {
- name: "health check",
- method: "GET",
- path: "/health",
- expectedStatus: http.StatusOK,
- expectedBody: `{"status":"healthy"}`,
- },
- {
- name: "unauthorized request",
- method: "POST",
- path: "/v1/mail/send",
- expectedStatus: http.StatusUnauthorized,
- },
- {
- name: "authorized mail request",
- method: "POST",
- path: "/v1/mail/send",
- headers: map[string]string{
- "X-API-Key": "test-api-key",
- "Content-Type": "application/json",
- },
- body: `{"name":"Test","email":"test@example.com","message":"Test"}`,
- expectedStatus: http.StatusOK,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- req := createTestRequest(t, tc.method, gateway.URL+tc.path, tc.body)
-
- for key, value := range tc.headers {
- req.Header.Set(key, value)
- }
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- t.Fatalf("Request failed: %v", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != tc.expectedStatus {
- t.Errorf("Expected status %d, got %d", tc.expectedStatus, resp.StatusCode)
- }
-
- if tc.expectedBody != "" {
- body, _ := io.ReadAll(resp.Body)
- if string(body) != tc.expectedBody {
- t.Errorf("Expected body %s, got %s", tc.expectedBody, string(body))
- }
- }
- })
- }
-}
-```
-
-### 5.2 Service API Tests
-
-Tests für individuelle Service-APIs:
-
-```go
-func TestFormular2MailAPI(t *testing.T) {
- service := startTestFormular2MailService(t)
- defer service.Close()
-
- t.Run("valid mail request", func(t *testing.T) {
- reqBody := `{
- "name": "John Doe",
- "email": "john@example.com",
- "message": "Test message"
- }`
-
- resp := makeTestRequest(t, service.URL+"/send", "POST", reqBody)
- assertStatusCode(t, resp, http.StatusOK)
-
- var response MailResponse
- if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
- t.Fatalf("Failed to decode response: %v", err)
- }
-
- if !response.Success {
- t.Error("Expected success=true in response")
- }
- })
-
- t.Run("invalid mail request", func(t *testing.T) {
- reqBody := `{"name": "", "email": "invalid", "message": ""}`
-
- resp := makeTestRequest(t, service.URL+"/send", "POST", reqBody)
- assertStatusCode(t, resp, http.StatusBadRequest)
- })
-}
-```
-
-## 6. Performance Tests
-
-### 6.1 Gateway Performance Tests
-
-```go
-func TestGatewayPerformance(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping performance test in short mode")
- }
-
- gateway := setupTestGateway(t)
- defer gateway.Close()
-
- // Load test
- concurrency := 10
- requests := 1000
-
- var wg sync.WaitGroup
- errors := make(chan error, requests)
-
- start := time.Now()
-
- for i := 0; i < concurrency; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for j := 0; j < requests/concurrency; j++ {
- resp, err := http.Get(gateway.URL + "/health")
- if err != nil {
- errors <- err
- return
- }
- resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- errors <- fmt.Errorf("unexpected status: %d", resp.StatusCode)
- return
- }
- }
- }()
- }
-
- wg.Wait()
- close(errors)
-
- duration := time.Since(start)
-
- // Check for errors
- for err := range errors {
- t.Errorf("Request error: %v", err)
- }
-
- // Performance assertions
- requestsPerSecond := float64(requests) / duration.Seconds()
- if requestsPerSecond < 500 { // Minimum 500 RPS
- t.Errorf("Performance too low: %.2f RPS", requestsPerSecond)
- }
-
- t.Logf("Performance: %.2f RPS over %v", requestsPerSecond, duration)
-}
-
-func BenchmarkGatewayRouting(b *testing.B) {
- gateway := setupBenchmarkGateway(b)
-
- req := httptest.NewRequest("GET", "/v1/mail/send", nil)
- req.Header.Set("X-API-Key", "test-key")
-
- b.ResetTimer()
-
- for i := 0; i < b.N; i++ {
- w := httptest.NewRecorder()
- gateway.ServeHTTP(w, req)
- }
-}
-```
-
-## 7. Test-Daten und Test-Utilities
-
-### 7.1 Test-Daten-Management
-
-```go
-// internal/testutil/fixtures.go
-package testutil
-
-func CreateTestMailRequest() MailRequest {
- return MailRequest{
- Name: "Test User",
- Email: "test@example.com",
- Subject: "Test Subject",
- Message: "Test Message",
- }
-}
-
-func CreateTestComment() *Comment {
- return &Comment{
- ID: uuid.New().String(),
- PageURL: "https://example.com/test",
- Author: "Test Author",
- Email: "test@example.com",
- Content: "Test Comment Content",
- Status: StatusPending,
- }
-}
-
-func CreateTestGatewayConfig() GatewayConfig {
- return GatewayConfig{
- Gateway: GatewaySettings{
- Port: "8080",
- LogLevel: "info",
- },
- Security: SecurityConfig{
- APIKeys: []APIKey{
- {
- Key: "test-api-key",
- Name: "Test Key",
- Permissions: []string{"mail:send"},
- AllowedIPs: []string{"127.0.0.1"},
- },
- },
- },
- Services: map[string]ServiceConfig{
- "formular2mail": {
- Enabled: true,
- PathPrefix: "/v1/mail",
- Upstream: "http://127.0.0.1:8081",
- HealthCheck: "/health",
- Timeout: 30 * time.Second,
- },
- },
- }
-}
-```
-
-### 7.2 Test-Helper-Funktionen
-
-```go
-// internal/testutil/helpers.go
-package testutil
-
-func AssertStatusCode(t *testing.T, resp *http.Response, expected int) {
- t.Helper()
- if resp.StatusCode != expected {
- t.Errorf("Expected status code %d, got %d", expected, resp.StatusCode)
- }
-}
-
-func AssertResponseBody(t *testing.T, resp *http.Response, expected string) {
- t.Helper()
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- t.Fatalf("Failed to read response body: %v", err)
- }
-
- if string(body) != expected {
- t.Errorf("Expected body %q, got %q", expected, string(body))
- }
-}
-
-func MakeTestRequest(t *testing.T, url, method, body string) *http.Response {
- t.Helper()
-
- var reqBody io.Reader
- if body != "" {
- reqBody = strings.NewReader(body)
- }
-
- req, err := http.NewRequest(method, url, reqBody)
- if err != nil {
- t.Fatalf("Failed to create request: %v", err)
- }
-
- if body != "" {
- req.Header.Set("Content-Type", "application/json")
- }
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- t.Fatalf("Request failed: %v", err)
- }
-
- return resp
-}
-```
-
-## 8. Test-Umgebung und CI
-
-### 8.1 Lokale Tests
-
-- Alle Tests sollten mit `go test ./...` ausführbar sein
-- Keine Tests sollten externe Ressourcen benötigen (wie echte E-Mail-Server)
-- Performance-Tests mit `-short` Flag überspringen
-
-### 8.2 Test-Tags
-
-```go
-// +build integration
-
-package tests
-
-// Integration tests that require external resources
-```
-
-**Ausführung:**
-```bash
-# Nur Unit Tests
-go test ./...
-
-# Mit Integration Tests
-go test -tags=integration ./...
-
-# Mit Performance Tests
-go test -timeout=30m ./...
-
-# Kurze Tests für CI
-go test -short ./...
-```
-
-### 8.3 Coverage-Berichte
-
-```bash
-# Coverage generieren
-go test -coverprofile=coverage.out ./...
-
-# HTML-Report
-go tool cover -html=coverage.out -o coverage.html
-
-# Coverage-Threshold prüfen
-go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//'
-```
-
-## 9. Spezifische Testfälle für Furt
-
-### 9.1 Gateway-Routing Tests
-
-```go
-func TestGatewayServiceRouting(t *testing.T) {
- testCases := []struct {
- name string
- requestPath string
- method string
- expectedService string
- expectedUpstream string
- wantErr bool
- }{
- {
- name: "formular2mail routing",
- requestPath: "/v1/mail/send",
- method: "POST",
- expectedService: "formular2mail",
- expectedUpstream: "http://127.0.0.1:8081",
- },
- {
- name: "sagjan comments routing",
- requestPath: "/v1/comments",
- method: "GET",
- expectedService: "sagjan",
- expectedUpstream: "http://127.0.0.1:8082",
- },
- {
- name: "unknown service",
- requestPath: "/v1/unknown",
- method: "GET",
- wantErr: true,
- },
- }
-
- // Implementation...
-}
-```
-
-### 9.2 Authentication Tests
-
-```go
-func TestGatewayAuthentication(t *testing.T) {
- testCases := []struct {
- name string
- apiKey string
- clientIP string
- requestPath string
- expectedStatus int
- }{
- {
- name: "valid API key and IP",
- apiKey: "hugo-frontend-key",
- clientIP: "127.0.0.1",
- requestPath: "/v1/mail/send",
- expectedStatus: http.StatusOK,
- },
- {
- name: "invalid API key",
- apiKey: "invalid-key",
- clientIP: "127.0.0.1",
- requestPath: "/v1/mail/send",
- expectedStatus: http.StatusUnauthorized,
- },
- {
- name: "blocked IP",
- apiKey: "hugo-frontend-key",
- clientIP: "192.168.1.100",
- requestPath: "/v1/mail/send",
- expectedStatus: http.StatusForbidden,
- },
- }
-
- // Implementation...
-}
-```
-
-### 9.3 Service Health Check Tests
-
-```go
-func TestServiceHealthChecks(t *testing.T) {
- // Test Gateway health aggregation
- t.Run("all services healthy", func(t *testing.T) {
- // Setup healthy services
- // Test /health returns 200 with all services status
- })
-
- t.Run("one service unhealthy", func(t *testing.T) {
- // Setup one failing service
- // Test /health returns appropriate status
- })
-}
-```
-
-## 10. Test-Automation und CI-Integration
-
-### 10.1 GitHub Actions / Gitea Actions
-
-```yaml
-# .gitea/workflows/test.yml
-name: Test Suite
-
-on: [push, pull_request]
-
-jobs:
- test:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Set up Go
- uses: actions/setup-go@v3
- with:
- go-version: 1.21
-
- - name: Run Unit Tests
- run: go test -short -race -coverprofile=coverage.out ./...
-
- - name: Run Integration Tests
- run: go test -tags=integration ./tests/integration/
-
- - name: Check Coverage
- run: |
- coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
- if (( $(echo "$coverage < 80" | bc -l) )); then
- echo "Coverage $coverage% is below 80%"
- exit 1
- fi
-```
-
-## 11. Best Practices Zusammenfassung
-
-### 11.1 Do's
-
-- ✅ **Testbare Architektur:** Dependency Injection verwenden
-- ✅ **Isolierte Tests:** Keine Abhängigkeiten zwischen Tests
-- ✅ **Realistische Test-Daten:** Aber anonymisiert und minimal
-- ✅ **Performance-bewusst:** Benchmarks für kritische Pfade
-- ✅ **Dokumentierte Test-Fälle:** Klare Beschreibungen der Test-Szenarien
-
-### 11.2 Don'ts
-
-- ❌ **Externe Ressourcen:** Keine echten E-Mail-Server, externe APIs
-- ❌ **Feste Zeitstempel:** `time.Now()` mocken in Tests
-- ❌ **Globaler State:** Tests sollten unabhängig sein
-- ❌ **Überflüssige Tests:** Triviale Getter/Setter nicht testen
-- ❌ **Fragile Tests:** Tests sollen bei kleinen Änderungen nicht brechen
-
----
-
-Diese Richtlinien sollen als Leitfaden dienen und können im Laufe des Projekts angepasst und erweitert werden. Bei Unklarheiten oder Fragen zu diesen Richtlinien kann das Entwicklungsteam kontaktiert werden.
\ No newline at end of file
diff --git a/docs/lua-implementation-reference.md b/docs/lua-implementation-reference.md
new file mode 100644
index 0000000..433d29f
--- /dev/null
+++ b/docs/lua-implementation-reference.md
@@ -0,0 +1,187 @@
+# Furt Lua HTTP-Server
+
+**Pure Lua HTTP-Server für Dragons@Work API-Gateway**
+*Week 1 Implementation - Digital Sovereignty Project*
+
+## Überblick
+
+Furt ist der erste Schritt zur Migration des API-Gateways von Go auf C+Lua für maximale digitale Souveränität. Diese Implementierung startet mit reinem Lua und bildet die Grundlage für die spätere C+Lua-Hybridarchitektur.
+
+## Funktionen
+
+- ✅ **HTTP-Server** mit lua-socket
+- ✅ **JSON API** Endpoints
+- ✅ **Request/Response Parsing**
+- ✅ **Basic Routing**
+- ✅ **Mail-Service-Grundgerüst**
+- ✅ **Health-Check**
+- ✅ **Error Handling**
+- ✅ **Automated Tests**
+
+## Dependencies
+
+**Erforderlich:**
+- `lua` 5.4+
+- `lua-socket` (HTTP-Server)
+- `lua-cjson` (JSON-Verarbeitung)
+
+**Arch Linux:**
+```bash
+pacman -S lua lua-socket lua-cjson
+```
+
+**Ubuntu:**
+```bash
+apt install lua5.4 lua-socket lua-cjson
+```
+
+## Projektstruktur
+
+```
+furt-lua/
+├── src/
+│ └── main.lua # HTTP-Server (< 200 Zeilen)
+├── config/
+│ └── server.lua # Konfiguration
+├── scripts/
+│ ├── start.sh # Server starten
+│ └── test_curl.sh # Manuelle Tests
+├── tests/
+│ └── test_http.lua # Automatische Tests
+└── README.md
+```
+
+## Installation & Start
+
+**1. Repository Setup:**
+```bash
+mkdir furt-lua
+cd furt-lua
+
+# Dateien erstellen (aus Claude-Artefakten)
+# main.lua, config/server.lua, scripts/start.sh, etc.
+```
+
+**2. Executable machen:**
+```bash
+chmod +x scripts/start.sh
+chmod +x scripts/test_curl.sh
+```
+
+**3. Server starten:**
+```bash
+./scripts/start.sh
+```
+
+**Server läuft auf:** http://127.0.0.1:8080
+
+## API-Endpoints
+
+### Health Check
+```bash
+GET /health
+→ {"status":"healthy","service":"furt-lua","version":"1.0.0"}
+```
+
+### Test Endpoint
+```bash
+POST /test
+Content-Type: application/json
+{"test": "data"}
+→ {"message":"Test endpoint working"}
+```
+
+### Mail Service
+```bash
+POST /v1/mail/send
+Content-Type: application/json
+{
+ "name": "Test User",
+ "email": "test@example.com",
+ "message": "Test message"
+}
+→ {"success":true,"message":"Mail queued for sending"}
+```
+
+## Testing
+
+**Automatische Tests:**
+```bash
+# Server muss laufen!
+lua tests/test_http.lua
+```
+
+**Manuelle curl-Tests:**
+```bash
+./scripts/test_curl.sh
+```
+
+**Quick Test:**
+```bash
+curl -X POST http://127.0.0.1:8080/test \
+ -H "Content-Type: application/json" \
+ -d '{"test":"data"}'
+```
+
+## Konfiguration
+
+**Mail-SMTP (Environment Variables):**
+```bash
+export FURT_MAIL_USERNAME="your_email@dragons-at-work.de"
+export FURT_MAIL_PASSWORD="your_password"
+```
+
+**Server-Config:** `config/server.lua`
+- Port, Host ändern
+- API-Keys definieren
+- SMTP-Einstellungen
+
+## Week 1 Status
+
+✅ **Tag 1:** HTTP-Server basic functionality
+✅ **Tag 2:** Request/Response parsing
+✅ **Tag 3:** JSON handling, Mail endpoint structure
+✅ **Tag 4:** Routing, Error handling
+✅ **Tag 5:** Testing, Documentation
+
+**Success Criteria erreicht:**
+- ✅ `curl -X POST http://localhost:8080/test` → HTTP 200 ✓
+- ✅ Alle Module < 200 Zeilen ✓
+- ✅ JSON Request/Response ✓
+- ✅ /v1/mail/send Endpoint ✓
+
+## Nächste Schritte (Week 2)
+
+1. **SMTP-Integration** - Echte Mail-Versendung
+2. **API-Key-Authentication** - Security-Layer
+3. **Hugo-Integration** - POST-based Form-Handling
+4. **HTTPS** mit lua-ssl
+
+## Technologie-Philosophie
+
+- **Lua:** PUC-Rio University (echte Unabhängigkeit)
+- **Minimale Dependencies:** < 5 externe Libraries
+- **Modulare Architektur:** < 200 Zeilen pro Datei
+- **Transparenter Code:** Jede Zeile verstehbar
+- **Corporate-frei:** Keine Google/Microsoft/etc. Dependencies
+
+**Teil der Dragons@Work Tech-Souveränitätsstrategie**
+
+## Development
+
+**Code-Stil:**
+- Module < 200 Zeilen
+- Funktionen < 50 Zeilen
+- Klare, lesbare Namen
+- Error-Handling für alles
+
+**Testing-Pattern:**
+- Jede Funktion testbar
+- HTTP-Integration-Tests
+- curl-basierte Verifikation
+
+---
+
+**Week 1 Challenge: COMPLETE ✅**
+*Foundation für souveräne API-Gateway-Architektur gelegt.*
+
diff --git a/docs/production_checklist.md b/docs/production_checklist.md
new file mode 100644
index 0000000..1351685
--- /dev/null
+++ b/docs/production_checklist.md
@@ -0,0 +1,139 @@
+# Furt API-Gateway Production Deployment Checklist
+
+## 🔐 Security Configuration
+
+### API Keys
+- [ ] Generate secure API keys (32+ characters)
+- [ ] Set HUGO_API_KEY in .env.production
+- [ ] Set ADMIN_API_KEY in .env.production
+- [ ] Remove/change all development keys
+- [ ] Verify API key permissions in config/server.lua
+
+### CORS Configuration
+- [ ] Set production domains in CORS_ALLOWED_ORIGINS
+- [ ] Remove localhost/development origins
+- [ ] Test CORS with production domains
+
+### Endpoints
+- [ ] Disable test endpoint (ENABLE_TEST_ENDPOINT=false)
+- [ ] Remove any debug endpoints
+- [ ] Verify only required endpoints are exposed
+
+## 📧 SMTP Configuration
+
+- [ ] Configure production SMTP server
+- [ ] Test SMTP authentication
+- [ ] Set proper FROM and TO addresses
+- [ ] Verify mail delivery works
+- [ ] Test mail sending with rate limits
+
+## 🔧 Server Configuration
+
+### Environment
+- [ ] Copy .env.production to .env
+- [ ] Set GATEWAY_HOST (127.0.0.1 for internal)
+- [ ] Set GATEWAY_PORT (8080 default)
+- [ ] Set LOG_LEVEL to "warn" or "error"
+
+### Performance
+- [ ] Verify rate limits are appropriate
+- [ ] Test concurrent load handling
+- [ ] Monitor memory usage under load
+- [ ] Test restart behavior
+
+## 🛡️ Security Testing
+
+### Authentication
+- [ ] Test invalid API keys return 401
+- [ ] Test missing API keys return 401
+- [ ] Test permission system works correctly
+- [ ] Test IP restrictions (if configured)
+
+### Rate Limiting
+- [ ] Test rate limits trigger at correct thresholds
+- [ ] Test 429 responses are returned
+- [ ] Test rate limit headers are present
+- [ ] Test rate limit cleanup works
+
+## 🚀 Deployment
+
+### File Permissions
+- [ ] Lua files readable by server user
+- [ ] .env file protected (600 permissions)
+- [ ] Log directory writable
+- [ ] No world-readable sensitive files
+
+### Process Management
+- [ ] Configure systemd service (if applicable)
+- [ ] Test automatic restart on failure
+- [ ] Configure log rotation
+- [ ] Set up monitoring/health checks
+
+### Reverse Proxy (if applicable)
+- [ ] Configure nginx/apache reverse proxy
+- [ ] Set up SSL termination
+- [ ] Configure rate limiting at proxy level
+- [ ] Test proxy → furt communication
+
+## 📊 Monitoring
+
+### Health Checks
+- [ ] /health endpoint responds correctly
+- [ ] Set up external monitoring (e.g., Uptime Kuma)
+- [ ] Configure alerting for service down
+- [ ] Test health check under load
+
+### Logging
+- [ ] Configure appropriate log level
+- [ ] Set up log rotation
+- [ ] Monitor log file sizes
+- [ ] Review error patterns
+
+### Metrics
+- [ ] Monitor request rates
+- [ ] Monitor response times
+- [ ] Monitor memory usage
+- [ ] Monitor SMTP connection health
+
+## 🧪 Integration Testing
+
+### Hugo Integration
+- [ ] Test contact forms submit successfully
+- [ ] Test error handling displays correctly
+- [ ] Test rate limiting shows user-friendly messages
+- [ ] Test CORS works with production domains
+
+### Mail Delivery
+- [ ] Send test emails through all forms
+- [ ] Verify emails arrive correctly formatted
+- [ ] Test email content encoding
+- [ ] Test attachment handling (if applicable)
+
+## 📝 Documentation
+
+- [ ] Document API endpoints for other developers
+- [ ] Document configuration options
+- [ ] Document troubleshooting procedures
+- [ ] Document backup/restore procedures
+
+## 🔄 Backup & Recovery
+
+- [ ] Document configuration files to backup
+- [ ] Test service restart procedures
+- [ ] Document rollback procedures
+- [ ] Test recovery from configuration errors
+
+## ✅ Final Verification
+
+- [ ] All API endpoints respond correctly
+- [ ] All security measures tested
+- [ ] Performance meets requirements
+- [ ] Monitoring and alerting configured
+- [ ] Documentation complete
+- [ ] Team trained on operations
+
+---
+
+**Last Updated:** $(date +%Y-%m-%d)
+**Deployed By:** _______________
+**Deployment Date:** _______________
\ No newline at end of file
diff --git a/furt_setup_repo.sh b/furt_setup_repo.sh
deleted file mode 100755
index 14d4bd9..0000000
--- a/furt_setup_repo.sh
+++ /dev/null
@@ -1,516 +0,0 @@
-#!/bin/bash
-
-set -e
-
-# Load environment variables
-if [ -f .env ]; then
- export $(cat .env | grep -v '^#' | xargs)
-else
- echo "❌ .env file not found!"
- echo "📋 Copy .env.example to .env and configure it first"
- exit 1
-fi
-
-# Validate required variables
-if [ -z "$GITEA_URL" ] || [ -z "$REPO_OWNER" ] || [ -z "$REPO_NAME" ] || [ -z "$GITEA_TOKEN" ]; then
- echo "❌ Missing required environment variables in .env"
- echo "📋 Check .env.example for required variables"
- exit 1
-fi
-
-# Colors
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m'
-
-log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
-log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
-log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
-log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
-
-# Check repo
-check_repo() {
- if [ ! -d ".git" ]; then
- log_error "Not in a Git repository!"
- exit 1
- fi
- log_success "Repository check passed"
-}
-
-# Create directory structure for API Gateway project
-create_directory_structure() {
- log_info "Creating Furt API Gateway directory structure..."
-
- # Core Go project structure
- mkdir -p cmd/{furt-gateway,services/{formular2mail,sagjan}}
- mkdir -p internal/{gateway,services/{formular2mail,sagjan},shared/{auth,config,logging}}
- mkdir -p pkg/client
-
- # Configuration and deployment
- mkdir -p configs/{services,examples}
- mkdir -p scripts/{build,deploy,development}
- mkdir -p tools/service-generator
-
- # Documentation
- mkdir -p docs/{api,installation,services}
- mkdir -p devdocs
- mkdir -p examples/{hugo,nginx,apache,docker}
-
- # Testing
- mkdir -p tests/{unit,integration,e2e}
-
- # Gitea specific
- mkdir -p .gitea/{issue_template,workflows}
-
- log_success "Furt directory structure created"
-}
-
-# Create issue templates for API project
-create_issue_templates() {
- log_info "Creating Furt-specific issue templates..."
-
- # Service Request Template
- cat > .gitea/issue_template/service_request.yml << 'TEMPLATE_EOF'
-name: 🔧 Neuer Service für API-Gateway
-description: Anfrage für einen neuen Service im Furt-Gateway
-title: "[SERVICE] "
-labels: ["service-request", "enhancement"]
-body:
- - type: input
- id: service_name
- attributes:
- label: "🏷️ Service-Name"
- description: "Wie soll der neue Service heißen?"
- placeholder: "z.B. newsletter, shop, calendar"
- validations:
- required: true
-
- - type: textarea
- id: service_description
- attributes:
- label: "📝 Service-Beschreibung"
- description: "Was soll der Service tun?"
- placeholder: "Detaillierte Beschreibung der gewünschten Funktionalität"
- validations:
- required: true
-
- - type: input
- id: service_port
- attributes:
- label: "🔌 Gewünschter Port"
- description: "Auf welchem Port soll der Service laufen?"
- placeholder: "z.B. 8083, 8084"
-
- - type: dropdown
- id: priority
- attributes:
- label: "⚡ Priorität"
- description: "Wie dringend wird der Service benötigt?"
- options:
- - "🔥 Hoch - wird sofort benötigt"
- - "📊 Mittel - geplante Entwicklung"
- - "📝 Niedrig - nice to have"
- validations:
- required: true
-
- - type: checkboxes
- id: integration_needs
- attributes:
- label: "🔗 Integration-Anforderungen"
- description: "Welche Integrationen werden benötigt?"
- options:
- - label: "Hugo-Shortcode"
- - label: "OpenAPI-Dokumentation"
- - label: "Admin-Interface"
- - label: "E-Mail-Benachrichtigungen"
- - label: "Datenbank-Speicherung"
-TEMPLATE_EOF
-
- # Bug Report Template
- cat > .gitea/issue_template/bug_report.yml << 'TEMPLATE_EOF'
-name: 🐛 Bug Report
-description: Problem mit Gateway oder Service melden
-title: "[BUG] "
-labels: ["bug"]
-body:
- - type: dropdown
- id: component
- attributes:
- label: "🎯 Betroffene Komponente"
- description: "Welcher Teil des Systems ist betroffen?"
- options:
- - "Gateway (Routing, Auth, etc.)"
- - "Service: formular2mail"
- - "Service: sagjan"
- - "Konfiguration"
- - "Deployment/Scripts"
- - "Dokumentation"
- validations:
- required: true
-
- - type: textarea
- id: bug_description
- attributes:
- label: "📝 Bug-Beschreibung"
- description: "Was ist das Problem?"
- placeholder: "Detaillierte Beschreibung des Bugs"
- validations:
- required: true
-
- - type: textarea
- id: steps_to_reproduce
- attributes:
- label: "🔄 Schritte zur Reproduktion"
- description: "Wie kann der Bug reproduziert werden?"
- placeholder: |
- 1. Gehe zu ...
- 2. Klicke auf ...
- 3. Führe aus ...
- 4. Fehler tritt auf
- validations:
- required: true
-
- - type: textarea
- id: expected_behavior
- attributes:
- label: "✅ Erwartetes Verhalten"
- description: "Was sollte stattdessen passieren?"
- validations:
- required: true
-TEMPLATE_EOF
-
- # Architecture Discussion Template
- cat > .gitea/issue_template/architecture.yml << 'TEMPLATE_EOF'
-name: 🏗️ Architektur-Diskussion
-description: Diskussion über technische Entscheidungen und Architektur
-title: "[ARCH] "
-labels: ["architecture", "discussion"]
-body:
- - type: input
- id: topic
- attributes:
- label: "🎯 Thema"
- description: "Welcher Architektur-Aspekt soll diskutiert werden?"
- placeholder: "z.B. Service-Discovery, Auth-Strategy, Database-Choice"
- validations:
- required: true
-
- - type: textarea
- id: current_situation
- attributes:
- label: "📊 Aktuelle Situation"
- description: "Wie ist es momentan gelöst?"
-
- - type: textarea
- id: proposed_change
- attributes:
- label: "💡 Vorgeschlagene Änderung"
- description: "Was soll geändert/diskutiert werden?"
- validations:
- required: true
-
- - type: textarea
- id: alternatives
- attributes:
- label: "🔄 Alternativen"
- description: "Welche anderen Ansätze gibt es?"
-
- - type: checkboxes
- id: impact_areas
- attributes:
- label: "📈 Betroffene Bereiche"
- description: "Welche Teile des Systems sind betroffen?"
- options:
- - label: "Gateway-Performance"
- - label: "Service-Integration"
- - label: "Sicherheit"
- - label: "Skalierbarkeit"
- - label: "Wartbarkeit"
- - label: "Deployment"
-TEMPLATE_EOF
-
- log_success "Furt issue templates created"
-}
-
-# Create labels for API Gateway project
-create_labels() {
- log_info "Creating Furt-specific labels via Gitea API..."
-
- declare -a labels=(
- "gateway,0052CC,API-Gateway Kern-Funktionalität"
- "service-formular2mail,2188FF,Formular-zu-E-Mail Service"
- "service-sagjan,34D058,Sagjan Kommentarsystem Integration"
- "service-request,0E8A16,Anfrage für neuen Service"
- "architecture,6F42C1,Architektur und Design-Entscheidungen"
- "security,D73A49,Sicherheit und Authentifizierung"
- "performance,F66A0A,Performance und Optimierung"
- "documentation,D1D5DA,Dokumentation schreiben/verbessern"
- "testing,28A745,Tests und Qualitätssicherung"
- "deployment,FBCA04,Build, Deploy und DevOps"
- "configuration,008672,Konfiguration und Setup"
- "bug,DC143C,Fehler und Probleme"
- "enhancement,32CD32,Verbesserung oder neue Funktion"
- "question,87CEEB,Frage oder Hilfe benötigt"
- "help-wanted,FF69B4,Community-Input erwünscht"
- "good-first-issue,98FB98,Gut für neue Mitwirkende"
- "breaking-change,FF4500,Breaking Change - Version Bump nötig"
- "low-tech,8B4513,Im Einklang mit Low-Tech-Prinzipien"
- "digital-sovereignty,4B0082,Fördert digitale Souveränität"
- )
-
- for label_data in "${labels[@]}"; do
- IFS=',' read -r name color description <<< "$label_data"
-
- response=$(curl -s -w "\n%{http_code}" -X POST \
- "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/labels" \
- -H "Authorization: token $GITEA_TOKEN" \
- -H "Content-Type: application/json" \
- -d "{
- \"name\": \"$name\",
- \"color\": \"$color\",
- \"description\": \"$description\"
- }")
-
- http_code=$(echo "$response" | tail -n1)
- if [ "$http_code" = "201" ]; then
- log_success "Label '$name' created"
- elif [ "$http_code" = "409" ]; then
- log_warning "Label '$name' already exists"
- else
- log_error "Failed to create label '$name' (HTTP: $http_code)"
- fi
- done
-}
-
-# Create .env.example for API project
-create_env_example() {
- log_info "Creating .env.example for Furt..."
-
- cat > .env.example << 'ENV_EOF'
-# Gitea-Konfiguration für Issue-Management
-GITEA_URL=https://your-gitea-instance.com
-REPO_OWNER=your-username
-REPO_NAME=furt
-GITEA_TOKEN=your-gitea-token-here
-
-# Optional: Default-Assignee für Issues
-DEFAULT_ASSIGNEE=your-username
-
-# Gateway-Konfiguration (für Entwicklung)
-GATEWAY_PORT=8080
-GATEWAY_LOG_LEVEL=info
-
-# Service-Ports (für lokale Entwicklung)
-FORMULAR2MAIL_PORT=8081
-SAGJAN_PORT=8082
-
-# SMTP-Konfiguration (für formular2mail)
-SMTP_HOST=localhost
-SMTP_PORT=25
-SMTP_FROM=no-reply@dragons-at-work.de
-SMTP_TO=admin@dragons-at-work.de
-
-# API-Schlüssel (generiere sichere Schlüssel für Produktion!)
-HUGO_API_KEY=change-me-in-production
-ADMIN_API_KEY=change-me-in-production
-ENV_EOF
-
- log_success ".env.example created"
-}
-
-# Update .gitignore for Go project
-update_gitignore() {
- log_info "Creating Go-specific .gitignore..."
-
- cat > .gitignore << 'GITIGNORE_EOF'
-# Environment variables (NEVER commit!)
-.env
-
-# Go build artifacts
-*.exe
-*.exe~
-*.dll
-*.so
-*.dylib
-furt-gateway
-formular2mail-service
-sagjan-service
-/build/
-/dist/
-
-# Go test files
-*.test
-*.out
-coverage.txt
-coverage.html
-
-# Go modules
-/vendor/
-
-# OS generated files
-.DS_Store
-.DS_Store?
-._*
-.Spotlight-V100
-.Trashes
-ehthumbs.db
-Thumbs.db
-
-# Editor files
-.vscode/
-.idea/
-*.swp
-*.swo
-*~
-
-# Temporary files
-*.tmp
-*.temp
-*.log
-
-# Development files
-_personal/
-_drafts/
-_notes/
-debug.log
-
-# Database files (for testing)
-*.db
-*.sqlite
-*.sqlite3
-
-# Configuration files with secrets
-config.local.yaml
-config.production.yaml
-GITIGNORE_EOF
-
- log_success ".gitignore updated for Go project"
-}
-
-# Create initial Go module
-create_go_module() {
- log_info "Initializing Go module..."
-
- if [ ! -f "go.mod" ]; then
- go mod init furt
- log_success "Go module initialized"
- else
- log_warning "go.mod already exists"
- fi
-}
-
-# Create basic project files
-create_basic_files() {
- log_info "Creating basic project files..."
-
- # README.md
- cat > README.md << 'README_EOF'
-# Furt API Gateway
-
-Ein Low-Tech API-Gateway für selbst-gehostete Services im Einklang mit digitaler Souveränität.
-
-## Überblick
-
-Furt ist ein minimalistischer API-Gateway, der verschiedene Services unter einer einheitlichen API vereint. Der Name "Furt" (germanisch für "Durchgang durch Wasser") symbolisiert die Gateway-Funktion: Alle Requests durchqueren die API-Furt um zu den dahinterliegenden Services zu gelangen.
-
-## Philosophie
-
-- **Low-Tech-Ansatz**: Einfachheit vor Komplexität
-- **Digitale Souveränität**: Vollständige Kontrolle über die eigene Infrastruktur
-- **Native Deployment**: Go-Binaries ohne externe Abhängigkeiten
-- **Ressourcenschonend**: Minimaler Speicher- und CPU-Verbrauch
-- **Open Source**: Transparent und gemeinschaftlich entwickelt
-
-## Status
-
-🚧 **In Entwicklung** - Grundgerüst wird implementiert
-
-## Geplante Services
-
-- **formular2mail**: Kontaktformulare zu E-Mail weiterleiten
-- **sagjan**: Selbst-gehostetes Kommentarsystem
-- **Weitere**: Shop, Newsletter, Terminbuchung, etc.
-
-## Installation
-
-*Dokumentation folgt mit erstem Release*
-
-## Entwicklung
-
-Siehe `devdocs/` für Entwicklungsrichtlinien und Architektur-Dokumentation.
-
-## Lizenz
-
-Apache License 2.0 - Siehe [LICENSE](LICENSE) für Details.
-README_EOF
-
- # LICENSE
- cat > LICENSE << 'LICENSE_EOF'
-Apache License
-Version 2.0, January 2004
-http://www.apache.org/licenses/
-
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-[Complete Apache 2.0 license text would go here]
-LICENSE_EOF
-
- log_success "Basic project files created"
-}
-
-# Git commit and push
-commit_and_push() {
- log_info "Committing initial Furt structure..."
-
- git add .
- git commit -m "feat: Initiale Furt API-Gateway Projektstruktur
-
-- Go-Projektstruktur nach Low-Tech-Prinzipien
-- Issue-Templates für Service-Requests und Bug-Reports
-- Konfiguration für sichere Entwicklung (.env.example)
-- Scripts-Verzeichnis für Build und Deployment
-- Dokumentationsstruktur für Dev und User Docs
-- Apache 2.0 Lizenz für Open-Source-Entwicklung
-
-Furt (Durchgang) vereint Services unter einheitlicher API
-für vollständige digitale Souveränität."
-
- if git remote get-url origin > /dev/null 2>&1; then
- git push origin main
- log_success "Changes committed and pushed"
- else
- log_warning "No remote 'origin' configured - changes committed locally"
- fi
-}
-
-# Main function
-main() {
- log_info "🚀 Starting Furt API Gateway repository setup"
- echo
-
- check_repo
- create_directory_structure
- create_issue_templates
- create_env_example
- update_gitignore
- create_basic_files
- create_go_module
- commit_and_push
- create_labels
-
- echo
- log_success "🎯 Furt repository setup complete!"
- echo
- echo "Next steps:"
- echo "1. Copy .env.example to .env and configure it"
- echo "2. Create devdocs/KONZEPT.md with project philosophy"
- echo "3. Implement Gateway basic structure in cmd/furt-gateway/"
- echo "4. Create first service: formular2mail"
- echo "5. Test with Hugo integration"
- echo
- log_info "Ready to build the Furt! 🌊"
-}
-
-main "$@"
\ No newline at end of file
diff --git a/go.mod b/go.mod
deleted file mode 100644
index 4f8e1c1..0000000
--- a/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module furt
-
-go 1.24.3
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..4711c2b
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,122 @@
+#!/bin/sh
+# install.sh - furt Installation and Update Orchestrator
+
+set -e
+
+# Parse command line arguments
+UPGRADE_MODE=false
+SKIP_USER=false
+SKIP_SERVICE=false
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --upgrade) UPGRADE_MODE=true; shift ;;
+ --skip-user) SKIP_USER=true; shift ;;
+ --skip-service) SKIP_SERVICE=true; shift ;;
+ --help)
+ echo "Usage: $0 [--upgrade] [--skip-user] [--skip-service]"
+ echo " --upgrade Update existing installation (skip user/service creation)"
+ echo " --skip-user Skip user creation step"
+ echo " --skip-service Skip service creation step"
+ exit 0
+ ;;
+ *) echo "Unknown option: $1"; exit 1 ;;
+ esac
+done
+
+# Validate we're in furt source directory
+if [ ! -f "src/main.lua" ] || [ ! -d "scripts" ]; then
+ echo "Error: Not in furt source directory"
+ echo "Expected files: src/main.lua, scripts/ directory"
+ exit 1
+fi
+
+echo "=== furt Installation ==="
+if [ "$UPGRADE_MODE" = "true" ]; then
+ echo "Mode: Upgrade (preserving config and service)"
+else
+ echo "Mode: Fresh installation"
+fi
+
+# Step 1: Create system user (skip in upgrade mode)
+if [ "$UPGRADE_MODE" = "false" ] && [ "$SKIP_USER" = "false" ]; then
+ echo "\n[1/6] Creating system user..."
+ ./scripts/setup-user.sh
+else
+ echo "\n[1/6] Skipping user creation (upgrade mode)"
+fi
+
+# Step 2: Setup directories
+echo "\n[2/6] Setting up directories..."
+./scripts/setup-directories.sh
+
+# Step 3: Sync source files (always needed for updates)
+echo "\n[3/6] Syncing source files..."
+./scripts/sync-files.sh
+
+# Step 4: Create service (skip in upgrade mode unless requested)
+if [ "$UPGRADE_MODE" = "false" ] && [ "$SKIP_SERVICE" = "false" ]; then
+ echo "\n[4/6] Creating system service..."
+ ./scripts/create-service.sh
+else
+ echo "\n[4/6] Skipping service creation (upgrade mode)"
+fi
+
+# Step 5: Validate configuration
+echo "\n[5/6] Validating configuration..."
+if ./scripts/validate-config.sh; then
+ echo "Configuration validation successful"
+else
+ echo "Warning: Configuration validation failed - manual setup may be needed"
+fi
+
+# Step 6: Health check
+echo "\n[6/6] Performing health check..."
+if ./scripts/health-check.sh >/dev/null 2>&1; then
+ echo "Health check passed - furt is running"
+else
+ echo "Health check failed - service may need to be started manually"
+fi
+
+# Installation summary
+echo "\n=== Installation Summary ==="
+if [ "$UPGRADE_MODE" = "true" ]; then
+ echo "furt upgrade completed successfully"
+ echo ""
+ echo "Source code updated to:"
+ if [ -f "/usr/local/share/furt/VERSION" ]; then
+ echo " Version: $(cat /usr/local/share/furt/VERSION)"
+ fi
+ if [ -f "/usr/local/share/furt/.version_history" ]; then
+ echo " Version history available (for furt internal tracking)"
+ fi
+ echo ""
+ echo "Service restart required:"
+ if [ "$(uname)" = "OpenBSD" ]; then
+ echo " doas rcctl restart furt"
+ else
+ echo " sudo systemctl restart furt"
+ fi
+else
+ echo "furt installation completed successfully"
+ echo ""
+ echo "Next steps:"
+ echo "1. Edit configuration file:"
+ if [ "$(uname)" = "OpenBSD" ]; then
+ echo " /usr/local/etc/furt/furt.conf"
+ else
+ echo " /etc/furt/furt.conf"
+ fi
+ echo "2. Start the service:"
+ if [ "$(uname)" = "OpenBSD" ]; then
+ echo " doas rcctl start furt"
+ else
+ echo " sudo systemctl start furt"
+ fi
+ echo "3. Test the API:"
+ echo " curl http://127.0.0.1:7811/health"
+fi
+
+echo ""
+echo "Installation log available in system logs"
+
diff --git a/integrations/lua-api.lua b/integrations/lua-api.lua
new file mode 100644
index 0000000..b543bce
--- /dev/null
+++ b/integrations/lua-api.lua
@@ -0,0 +1,352 @@
+-- integrations/lua-api.lua - merkwerk Lua API integration
+-- Provides merkwerk version information for Lua applications
+
+local merkwerk = {}
+
+-- Cache for version info to avoid repeated shell calls
+local cache = {
+ data = nil,
+ timestamp = 0,
+ ttl = 300 -- 5 minutes default TTL
+}
+
+-- Find merkwerk binary using universal detection pattern
+local function find_merkwerk_binary()
+ -- Check development binary
+ local dev_handle = io.popen("test -x './bin/merkwerk' && echo './bin/merkwerk' 2>/dev/null")
+ if dev_handle then
+ local dev_result = dev_handle:read("*line")
+ dev_handle:close()
+ if dev_result and dev_result ~= "" then
+ return dev_result
+ end
+ end
+
+ -- Check installed binary
+ local inst_handle = io.popen("test -x '/usr/local/bin/merkwerk' && echo '/usr/local/bin/merkwerk' 2>/dev/null")
+ if inst_handle then
+ local inst_result = inst_handle:read("*line")
+ inst_handle:close()
+ if inst_result and inst_result ~= "" then
+ return inst_result
+ end
+ end
+
+ -- Check PATH
+ local path_handle = io.popen("command -v merkwerk 2>/dev/null")
+ if path_handle then
+ local path_result = path_handle:read("*line")
+ path_handle:close()
+ if path_result and path_result ~= "" then
+ return "merkwerk"
+ end
+ end
+
+ return nil
+end
+
+-- Execute merkwerk command and return result
+local function execute_merkwerk(args)
+ args = args or "info --json"
+
+ local merkwerk_cmd = find_merkwerk_binary()
+ if not merkwerk_cmd then
+ return nil, "merkwerk binary not found"
+ end
+
+ local command = merkwerk_cmd .. " " .. args .. " 2>/dev/null"
+
+ local handle = io.popen(command)
+ if not handle then
+ return nil, "Failed to execute merkwerk command"
+ end
+
+ local result = handle:read("*a")
+ local success, exit_reason, exit_code = handle:close()
+
+ if not success or (exit_code and exit_code ~= 0) then
+ return nil, "merkwerk command failed with exit code " .. (exit_code or "unknown")
+ end
+
+ if result == "" then
+ return nil, "merkwerk returned empty result"
+ end
+
+ return result, nil
+end
+
+-- Parse JSON response (simple parser for basic merkwerk JSON)
+local function parse_json_response(json_str)
+ if not json_str then return nil end
+
+ -- Try to use cjson if available
+ local ok, cjson = pcall(require, "cjson")
+ if ok then
+ local success, data = pcall(cjson.decode, json_str)
+ if success then return data end
+ end
+
+ -- Fallback: simple manual parsing for merkwerk JSON structure
+ local data = {}
+
+ -- Extract basic fields using pattern matching
+ data.project_name = json_str:match('"project_name"%s*:%s*"([^"]*)"') or "unknown"
+ data.project_type = json_str:match('"project_type"%s*:%s*"([^"]*)"') or "unknown"
+ data.base_version = json_str:match('"base_version"%s*:%s*"([^"]*)"') or "?.?.?"
+ data.content_hash = json_str:match('"content_hash"%s*:%s*"([^"]*)"') or "unknown"
+ data.full_version = json_str:match('"full_version"%s*:%s*"([^"]*)"') or "?.?.?+unknown"
+ data.timestamp = json_str:match('"timestamp"%s*:%s*"([^"]*)"') or ""
+
+ -- Extract VCS info
+ local vcs_block = json_str:match('"vcs"%s*:%s*{([^}]*)}')
+ if vcs_block then
+ data.vcs = {}
+ data.vcs.type = vcs_block:match('"type"%s*:%s*"([^"]*)"') or "none"
+ data.vcs.hash = vcs_block:match('"hash"%s*:%s*"([^"]*)"') or ""
+ data.vcs.branch = vcs_block:match('"branch"%s*:%s*"([^"]*)"') or ""
+ else
+ data.vcs = { type = "none", hash = "", branch = "" }
+ end
+
+ return data
+end
+
+-- Read latest entry from .version_history file
+local function read_version_history()
+ local file = io.open(".version_history", "r")
+ if not file then
+ return nil, "No .version_history file found"
+ end
+
+ local last_line = nil
+ for line in file:lines() do
+ -- Skip comment lines
+ if not line:match("^%s*#") and line:match("%S") then
+ last_line = line
+ end
+ end
+ file:close()
+
+ if not last_line then
+ return nil, ".version_history contains no valid entries"
+ end
+
+ -- Parse: content_hash,vcs_hash,branch,timestamp,author,vcs_type,project_type
+ local parts = {}
+ for part in last_line:gmatch("([^,]+)") do
+ table.insert(parts, part)
+ end
+
+ if #parts < 7 then
+ return nil, "Invalid .version_history format"
+ end
+
+ -- Get base version from VERSION file if available
+ local base_version = "?.?.?"
+ local version_file = io.open("VERSION", "r")
+ if version_file then
+ local version_content = version_file:read("*line")
+ if version_content and not version_content:match("^%s*$") then
+ base_version = version_content:match("^%s*(.-)%s*$")
+ end
+ version_file:close()
+ end
+
+ -- Build response in same format as merkwerk
+ local data = {
+ project_name = parts[7] and parts[7]:gsub("-api$", "") or "unknown", -- lua-api → lua
+ project_type = parts[7] or "unknown",
+ base_version = base_version,
+ content_hash = parts[1] or "unknown",
+ full_version = base_version .. "+" .. (parts[1] or "unknown"),
+ version = base_version .. "+" .. (parts[1] or "unknown"),
+ timestamp = parts[4] or "",
+ vcs = {
+ type = parts[6] or "none",
+ hash = parts[2] or "",
+ branch = parts[3] or ""
+ },
+ source = "version_history"
+ }
+
+ return data, nil
+end
+
+-- Generate fallback info when merkwerk is unavailable
+local function fallback_info()
+ return {
+ project_name = "unknown",
+ project_type = "lua-api",
+ base_version = "?.?.?",
+ content_hash = "unknown",
+ full_version = "?.?.?+unknown",
+ timestamp = os.date("%Y-%m-%dT%H:%M:%SZ"),
+ vcs = {
+ type = "none",
+ hash = "",
+ branch = ""
+ },
+ source = "fallback",
+ error = "merkwerk not available"
+ }
+end
+
+-- Check if cached data is still valid
+local function is_cache_valid(ttl)
+ ttl = ttl or cache.ttl
+ local current_time = os.time()
+ return cache.data and (current_time - cache.timestamp) < ttl
+end
+
+-- Get version information with NEW priority order
+function merkwerk.get_info(options)
+ options = options or {}
+ local use_cache = options.cache ~= false
+ local cache_ttl = options.cache_ttl or cache.ttl
+ local fallback_version = options.fallback_version
+ local include_build_info = options.include_build_info or false
+
+ -- Return cached data if valid and caching enabled
+ if use_cache and is_cache_valid(cache_ttl) then
+ local result = cache.data
+ if include_build_info then
+ result.build_info = {
+ cached = true,
+ cache_age = os.time() - cache.timestamp,
+ cache_ttl = cache_ttl
+ }
+ end
+ return result
+ end
+
+ -- PRODUCTION PRIORITY: Try .version_history FIRST
+ local history_data, history_error = read_version_history()
+ if history_data then
+ -- Success: Use version history data
+ if include_build_info then
+ history_data.build_info = {
+ cached = false,
+ source = "version_history",
+ method = "production_ready"
+ }
+ end
+
+ -- Update cache
+ if use_cache then
+ cache.data = history_data
+ cache.timestamp = os.time()
+ end
+
+ return history_data
+ end
+
+ -- FALLBACK: Try merkwerk command (development/testing)
+ local json_result, error_msg = execute_merkwerk("info --json")
+ if json_result then
+ local data = parse_json_response(json_result)
+ if data then
+ data.source = "merkwerk"
+
+ if include_build_info then
+ data.build_info = {
+ cached = false,
+ source = "merkwerk_command",
+ method = "development_fallback",
+ history_error = history_error
+ }
+ end
+
+ -- Update cache
+ if use_cache then
+ cache.data = data
+ cache.timestamp = os.time()
+ end
+
+ return data
+ end
+ end
+
+ -- LAST RESORT: Pure fallback
+ local fallback = fallback_info()
+ if fallback_version then
+ fallback.base_version = fallback_version
+ fallback.full_version = fallback_version .. "+unknown"
+ end
+
+ fallback.error = "Both version_history and merkwerk failed"
+
+ if include_build_info then
+ fallback.build_info = {
+ cached = false,
+ source = "fallback",
+ method = "emergency_fallback",
+ history_error = history_error,
+ merkwerk_error = error_msg or "merkwerk unavailable"
+ }
+ end
+
+ return fallback
+end
+
+-- Get only the content hash (lightweight)
+function merkwerk.get_hash()
+ local hash_result, error_msg = execute_merkwerk("hash")
+
+ if not hash_result then
+ return "unknown"
+ end
+
+ -- Clean up the result (remove whitespace)
+ return hash_result:gsub("%s+", "")
+end
+
+-- Get version for HTTP health endpoints
+function merkwerk.get_health_info()
+ local info = merkwerk.get_info({ cache = true, cache_ttl = 600 }) -- 10 minute cache for health checks
+
+ return {
+ service = info.project_name,
+ version = info.full_version,
+ content_hash = info.content_hash,
+ vcs_info = info.vcs,
+ timestamp = info.timestamp,
+ source = info.source
+ }
+end
+
+-- Get minimal version string for logging
+function merkwerk.get_version_string()
+ local info = merkwerk.get_info({ cache = true })
+ return info.full_version
+end
+
+-- Clear cache (useful for testing or forced refresh)
+function merkwerk.clear_cache()
+ cache.data = nil
+ cache.timestamp = 0
+end
+
+-- Set cache TTL
+function merkwerk.set_cache_ttl(ttl)
+ cache.ttl = ttl or 300
+end
+
+-- Get cache status (for debugging)
+function merkwerk.get_cache_status()
+ return {
+ has_data = cache.data ~= nil,
+ timestamp = cache.timestamp,
+ age = cache.data and (os.time() - cache.timestamp) or 0,
+ ttl = cache.ttl,
+ valid = is_cache_valid()
+ }
+end
+
+-- Validate merkwerk availability
+function merkwerk.validate()
+ local result, error_msg = execute_merkwerk("info")
+ return result ~= nil, error_msg
+end
+
+return merkwerk
+
diff --git a/scripts/archive/create_issue_monster.sh b/scripts/archive/create_issue_monster.sh
deleted file mode 100755
index 6038c0c..0000000
--- a/scripts/archive/create_issue_monster.sh
+++ /dev/null
@@ -1,779 +0,0 @@
-#!/bin/bash
-# scripts/create_issue.sh - Furt API Gateway Issue Creator
-# DEBUG VERSION with path fixes and diagnostic output
-
-set -euo pipefail
-
-# Standard environment setup
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
-
-if [ -f "$PROJECT_ROOT/.env" ]; then
- export $(cat "$PROJECT_ROOT/.env" | grep -v '^#' | xargs)
-fi
-
-# Colors
-GREEN='\033[0;32m'
-BLUE='\033[0;34m'
-RED='\033[0;31m'
-YELLOW='\033[1;33m'
-CYAN='\033[0;36m'
-NC='\033[0m'
-
-log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
-log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
-log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
-log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
-log_debug() {
- if [[ "${DEBUG:-}" == "1" ]]; then
- echo -e "${CYAN}[DEBUG]${NC} $1" >&2;
- fi
-}
-
-# Track new labels for auto-update (FIXED: Safe initialization)
-declare -A NEW_LABELS_CREATED=()
-
-# === LABEL DEFINITIONS START ===
-# This section is auto-maintained by update_script_labels.sh
-# DO NOT EDIT MANUALLY - Changes will be overwritten
-declare -A LABEL_DEFINITIONS=(
- ["hugo-integration"]="color:ff7518;context:frontend;usage:hugo_templates,integration"
- ["service-newsletter"]="color:ff6b6b;context:newsletter;usage:newsletter_integration"
- ["service-analytics"]="color:1d76db;context:service_integration;usage:service_specific"
- ["ready-for-deployment"]="color:28a745;context:deploy_ready;usage:status_updates"
- ["service-clean-test4"]="color:1d76db;context:service_integration;usage:service_specific"
- ["service-completely-absolut-new7"]="color:1d76db;context:service_integration;usage:service_specific"
- ["service-completely-absolut-new9"]="color:1d76db;context:service_integration;usage:service_specific"
- ["service-completely-absolut-new8"]="color:1d76db;context:service_integration;usage:service_specific"
- ["performance"]="color:fbca04;context:optimization;usage:performance_template,architecture_template"
- ["bug"]="color:d73a4a;context:error;usage:bug_template,status_updates"
- ["question"]="color:d876e3;context:discussion;usage:question_template"
- ["service-formular2mail"]="color:1d76db;context:formular2mail;usage:formular2mail_integration"
- ["good-first-issue"]="color:7057ff;context:beginner_friendly;usage:manual_assignment"
- ["service-completely-absolut-new10"]="color:1d76db;context:service_integration;usage:service_specific"
- ["service-completely-absolut-new11"]="color:1d76db;context:service_integration;usage:service_specific"
- ["breaking-change"]="color:d73a4a;context:breaking;usage:api_templates,architecture_template"
- ["service-request"]="color:7057ff;context:new_service;usage:service_templates,status_updates"
- ["service-debug-test"]="color:1d76db;context:service_integration;usage:service_specific"
- ["low-priority"]="color:0e8a16;context:nice_to_have;usage:all_templates"
- ["blocked"]="color:d73a4a;context:blocked;usage:status_updates"
- ["low-tech"]="color:6f42c1;context:low_tech_principle;usage:architecture_template,performance_template,security_template"
- ["deployment"]="color:ff7518;context:deployment;usage:deployment_template"
- ["gateway"]="color:0052cc;context:gateway_core;usage:architecture_template,performance_template,service_templates"
- ["service-sagjan"]="color:1d76db;context:sagjan;usage:sagjan_integration"
- ["work-in-progress"]="color:fbca04;context:active;usage:status_updates"
- ["service-debug-check-final2"]="color:1d76db;context:service_integration;usage:service_specific"
- ["digital-sovereignty"]="color:6f42c1;context:digital_sovereignty;usage:architecture_template,performance_template,security_template"
- ["security"]="color:28a745;context:security_review;usage:security_template,architecture_template"
- ["architecture"]="color:d4c5f9;context:design;usage:architecture_template,gateway"
- ["configuration"]="color:f9d71c;context:config_management;usage:deployment_template,architecture_template"
- ["needs-review"]="color:0e8a16;context:review;usage:status_updates"
- ["help-wanted"]="color:159818;context:community_help;usage:manual_assignment"
- ["service-whatever-you-want"]="color:1d76db;context:service_integration;usage:service_specific"
- ["api-contract"]="color:5319e7;context:api_design;usage:api_templates,service_templates"
- ["enhancement"]="color:84b6eb;context:improvement;usage:all_templates"
- ["high-priority"]="color:d73a4a;context:urgent;usage:all_templates"
- ["testing"]="color:f9d71c;context:testing;usage:testing_template,integration"
- ["test-all-templates"]="color:ff0000;context:test;usage:all_templates"
-)
-
-# Extract label info
-get_label_color() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f1 | cut -d':' -f2; }
-get_label_context() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f2 | cut -d':' -f2; }
-get_label_usage() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f3 | cut -d':' -f2; }
-
-# Check if label is valid for context
-is_label_valid_for_context() {
- local label="$1"
- local context="$2"
- local usage=$(get_label_usage "$label")
- [[ "$usage" == *"$context"* ]] || [[ "$usage" == "all_templates" ]]
-}
-# === LABEL DEFINITIONS END ===
-
-# === TEMPLATE LABEL MAPPINGS START ===
-# Auto-generated template to label mappings
-declare -A TEMPLATE_LABELS=(
- ["performance"]="performance,low-priority,low-tech,gateway,digital-sovereignty,enhancement,high-priority,test-all-templates"
- ["bug"]="bug,low-priority,enhancement,high-priority,test-all-templates"
- ["api"]="breaking-change,low-priority,api-contract,enhancement,high-priority,test-all-templates"
- ["service"]="low-priority,gateway,api-contract,enhancement,high-priority,test-all-templates"
- ["deployment"]="low-priority,deployment,configuration,enhancement,high-priority,test-all-templates"
- ["security"]="low-priority,low-tech,digital-sovereignty,security,enhancement,high-priority,test-all-templates"
- ["architecture"]="performance,breaking-change,low-priority,low-tech,gateway,digital-sovereignty,security,architecture,configuration,enhancement,high-priority,test-all-templates"
- ["hugo"]="hugo-integration,low-priority,enhancement,high-priority,test-all-templates"
-)
-# === TEMPLATE LABEL MAPPINGS END ===
-
-# Load existing labels from repository
-declare -A LABEL_IDS
-
-load_existing_labels() {
- if [[ -z "${GITEA_URL:-}" ]] || [[ -z "${GITEA_TOKEN:-}" ]]; then
- log_error "GITEA_URL and GITEA_TOKEN must be set"
- exit 1
- fi
-
- log_info "Loading existing labels from repository..."
-
- local response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/labels" \
- -H "Authorization: token $GITEA_TOKEN")
-
- if [[ $? -ne 0 ]]; then
- log_error "Failed to fetch labels from repository"
- exit 1
- fi
-
- while IFS= read -r line; do
- local name=$(echo "$line" | jq -r '.name')
- local id=$(echo "$line" | jq -r '.id')
- LABEL_IDS["$name"]="$id"
- done < <(echo "$response" | jq -c '.[]')
-
- log_info "Loaded ${#LABEL_IDS[@]} existing labels"
-}
-
-# FIXED: Silent version of ensure_label_exists (no stdout pollution!)
-ensure_label_exists_silent() {
- local name="$1"
- local color="${2:-ff6b6b}"
- local description="${3:-Auto-generated label}"
- local usage="${4:-manual_assignment}" # ADDED: usage parameter
-
- log_debug "Checking label: $name"
-
- if [[ -n "${LABEL_IDS[$name]:-}" ]]; then
- log_debug "Label $name already exists (ID: ${LABEL_IDS[$name]})"
- return 0
- fi
-
- log_debug "Creating new label: $name with color $color"
-
- # Create label (redirect output to prevent stdout mixing)
- local response=$(curl -s -w "\n%{http_code}" -X POST \
- "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/labels" \
- -H "Authorization: token $GITEA_TOKEN" \
- -H "Content-Type: application/json" \
- -d "{
- \"name\": \"$name\",
- \"color\": \"$color\",
- \"description\": \"$description\"
- }" 2>/dev/null)
-
- local http_code=$(echo "$response" | tail -n1)
- local response_body=$(echo "$response" | head -n -1)
-
- if [[ "$http_code" == "201" ]]; then
- local new_id=$(echo "$response_body" | jq -r '.id')
- LABEL_IDS["$name"]="$new_id"
-
- # FIXED: Track for auto-update with correct usage
- NEW_LABELS_CREATED["$name"]="$color:auto_generated:$usage"
- log_debug "Successfully created label $name (ID: $new_id)"
- log_debug "Added to NEW_LABELS_CREATED: $name -> ${NEW_LABELS_CREATED[$name]}"
- return 0
- else
- log_debug "Failed to create label $name (HTTP: $http_code)"
- log_debug "Response: $response_body"
- return 1
- fi
-}
-
-# Process labels for template (updates global arrays, no output)
-process_labels_for_template() {
- local template="$1"
- shift
- local additional_labels=("$@")
-
- log_debug "Processing labels for template: $template"
- log_debug "Additional labels: ${additional_labels[*]}"
-
- # Get template labels
- local template_labels_string="${TEMPLATE_LABELS[$template]:-}"
- local all_labels=()
-
- # Add template labels
- if [[ -n "$template_labels_string" ]]; then
- IFS=',' read -ra template_labels <<< "$template_labels_string"
- all_labels+=("${template_labels[@]}")
- log_debug "Template labels: ${template_labels[*]}"
- fi
-
- # Add additional labels
- all_labels+=("${additional_labels[@]}")
- log_debug "All labels to process: ${all_labels[*]}"
-
- # Process all labels and ensure they exist
- for label in "${all_labels[@]}"; do
- log_debug "Processing label: $label"
-
- # Process both known and unknown labels
- if [[ -n "${LABEL_DEFINITIONS[$label]:-}" ]]; then
- log_debug "Known label: $label"
- # Known label - use defined color and context
- local color=$(get_label_color "$label")
- local context=$(get_label_context "$label")
-
- ensure_label_exists_silent "$label" "$color" "Furt: $context"
- else
- log_debug "Unknown label: $label - creating with smart defaults"
- # Unknown label - auto-create with smart defaults
- local default_color="ff6b6b"
- local default_context="auto_generated"
-
- # Smart defaults based on label pattern
- if [[ "$label" == service-* ]]; then
- default_color="1d76db"
- default_context="service_integration"
- default_usage="service_specific" # FIXED: Not all_templates!
- log_debug "Service label detected - using blue color and service_specific usage"
- elif [[ "$label" == *-priority ]]; then
- default_color="d73a4a"
- default_context="priority_level"
- default_usage="priority_management"
- log_debug "Priority label detected - using red color"
- elif [[ "$label" == hugo-* ]]; then
- default_color="ff7518"
- default_context="frontend_integration"
- default_usage="hugo_integration"
- log_debug "Hugo label detected - using orange color"
- else
- default_usage="manual_assignment"
- fi
-
- ensure_label_exists_silent "$label" "$default_color" "Furt: $default_context"
-
- # FIXED: Track with correct usage
- if [[ -n "${LABEL_IDS[$label]:-}" ]] && [[ -n "${NEW_LABELS_CREATED[$label]:-}" ]]; then
- NEW_LABELS_CREATED["$label"]="$default_color:$default_context:$default_usage"
- log_debug "Updated NEW_LABELS_CREATED with correct usage: $label -> $default_color:$default_context:$default_usage"
- fi
- fi
-
- # Debug: Check if this label was newly created
- if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
- if [[ -n "${NEW_LABELS_CREATED[$label]:-}" ]]; then
- log_debug " → Label $label was newly created and tracked"
- else
- log_debug " → Label $label already existed"
- fi
- else
- log_warning "Failed to process label: $label"
- fi
- done
-
- # Debug: Check NEW_LABELS_CREATED at end of processing
- log_debug "NEW_LABELS_CREATED after processing: ${#NEW_LABELS_CREATED[@]} entries"
- if [[ "${#NEW_LABELS_CREATED[@]}" -gt 0 ]] 2>/dev/null; then
- for label_name in "${!NEW_LABELS_CREATED[@]}"; do
- log_debug " - $label_name: ${NEW_LABELS_CREATED[$label_name]}"
- done
- else
- log_debug " (no entries in NEW_LABELS_CREATED array)"
- fi
-}
-
-# Build JSON from already processed labels (pure function, no side effects)
-build_labels_json_from_processed() {
- local template="$1"
- shift
- local additional_labels=("$@")
-
- log_debug "Building JSON from processed labels"
-
- # Get template labels
- local template_labels_string="${TEMPLATE_LABELS[$template]:-}"
- local all_labels=()
-
- # Add template labels
- if [[ -n "$template_labels_string" ]]; then
- IFS=',' read -ra template_labels <<< "$template_labels_string"
- all_labels+=("${template_labels[@]}")
- fi
-
- # Add additional labels
- all_labels+=("${additional_labels[@]}")
-
- # Collect IDs from already processed labels
- local label_ids=()
- for label in "${all_labels[@]}"; do
- if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
- label_ids+=("${LABEL_IDS[$label]}")
- log_debug "Added ID ${LABEL_IDS[$label]} for $label to JSON"
- else
- log_warning "No ID found for label: $label"
- fi
- done
-
- log_debug "Final label IDs for JSON: ${label_ids[*]}"
-
- # Build JSON array (clean output only!)
- if [[ ${#label_ids[@]} -gt 0 ]]; then
- printf '[%s]' "$(IFS=','; echo "${label_ids[*]}")"
- else
- echo "[]"
- fi
-}
-
-# DEPRECATED: Old build_labels_json function (kept for compatibility)
-build_labels_json() {
- local template="$1"
- shift
- local additional_labels=("$@")
-
- log_debug "Building labels for template: $template"
- log_debug "Additional labels: ${additional_labels[*]}"
-
- # Get template labels
- local template_labels_string="${TEMPLATE_LABELS[$template]:-}"
- local all_labels=()
-
- # Add template labels
- if [[ -n "$template_labels_string" ]]; then
- IFS=',' read -ra template_labels <<< "$template_labels_string"
- all_labels+=("${template_labels[@]}")
- log_debug "Template labels: ${template_labels[*]}"
- fi
-
- # Add additional labels
- all_labels+=("${additional_labels[@]}")
- log_debug "All labels to process: ${all_labels[*]}"
-
- # FIXED: Ensure all labels exist and collect IDs (handles unknown labels!)
- local label_ids=()
- for label in "${all_labels[@]}"; do
- log_debug "Processing label: $label"
-
- # Process both known and unknown labels
- if [[ -n "${LABEL_DEFINITIONS[$label]:-}" ]]; then
- log_debug "Known label: $label"
- # Known label - use defined color and context
- local color=$(get_label_color "$label")
- local context=$(get_label_context "$label")
-
- ensure_label_exists_silent "$label" "$color" "Furt: $context"
- else
- log_debug "Unknown label: $label - creating with smart defaults"
- # FIXED: Unknown label - auto-create with smart defaults
- local default_color="ff6b6b"
- local default_context="auto_generated"
-
- # Smart defaults based on label pattern
- if [[ "$label" == service-* ]]; then
- default_color="1d76db"
- default_context="service_integration"
- log_debug "Service label detected - using blue color"
- elif [[ "$label" == *-priority ]]; then
- default_color="d73a4a"
- default_context="priority_level"
- log_debug "Priority label detected - using red color"
- elif [[ "$label" == hugo-* ]]; then
- default_color="ff7518"
- default_context="frontend_integration"
- log_debug "Hugo label detected - using orange color"
- fi
-
- ensure_label_exists_silent "$label" "$default_color" "Furt: $default_context"
- fi
-
- # Collect ID if label was created/exists
- if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
- label_ids+=("${LABEL_IDS[$label]}")
- log_debug "Added label ID: ${LABEL_IDS[$label]} for $label"
-
- # Debug: Check if this label was newly created
- if [[ -n "${NEW_LABELS_CREATED[$label]:-}" ]]; then
- log_debug " → This label was newly created and tracked"
- else
- log_debug " → This label already existed"
- fi
- else
- log_warning "Failed to get ID for label: $label"
- fi
- done
-
- log_debug "Final label IDs: ${label_ids[*]}"
-
- # Debug: Check NEW_LABELS_CREATED at end of function
- log_debug "NEW_LABELS_CREATED at end of build_labels_json: ${#NEW_LABELS_CREATED[@]} entries"
- if [[ "${#NEW_LABELS_CREATED[@]}" -gt 0 ]] 2>/dev/null; then
- for label_name in "${!NEW_LABELS_CREATED[@]}"; do
- log_debug " - $label_name: ${NEW_LABELS_CREATED[$label_name]}"
- done
- else
- log_debug " (no entries in NEW_LABELS_CREATED array)"
- fi
-
- # Build JSON array (clean output only!)
- if [[ ${#label_ids[@]} -gt 0 ]]; then
- printf '[%s]' "$(IFS=','; echo "${label_ids[*]}")"
- else
- echo "[]"
- fi
-}
-
-# Show which labels are being used (AFTER JSON building to avoid stdout pollution)
-show_labels_used() {
- local template="$1"
- shift
- local additional_labels=("$@")
-
- log_info "Labels used for this issue:"
-
- # Show template labels
- local template_labels_string="${TEMPLATE_LABELS[$template]:-}"
- if [[ -n "$template_labels_string" ]]; then
- IFS=',' read -ra template_labels <<< "$template_labels_string"
- for label in "${template_labels[@]}"; do
- if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
- log_info " ✅ $label (ID: ${LABEL_IDS[$label]})"
- fi
- done
- fi
-
- # Show additional labels (these may have been newly created)
- for label in "${additional_labels[@]}"; do
- if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
- if [[ -n "${NEW_LABELS_CREATED[$label]:-}" ]]; then
- log_info " ✅ $label (ID: ${LABEL_IDS[$label]}) [NEW!]"
- else
- log_info " ✅ $label (ID: ${LABEL_IDS[$label]})"
- fi
- else
- log_warning " ❌ $label (failed to create)"
- fi
- done
-}
-
-# FIXED: AUTO-UPDATE with safe array handling and correct path
-auto_update_scripts_if_needed() {
- # FIXED: Safe check for empty associative array
- local new_labels_count=0
- if [[ "${#NEW_LABELS_CREATED[@]}" -gt 0 ]] 2>/dev/null; then
- new_labels_count=${#NEW_LABELS_CREATED[@]}
- fi
-
- log_debug "Auto-update check: $new_labels_count new labels created"
-
- # Debug: Show what's in NEW_LABELS_CREATED
- if [[ $new_labels_count -gt 0 ]]; then
- log_debug "NEW_LABELS_CREATED contents:"
- for label_name in "${!NEW_LABELS_CREATED[@]}"; do
- log_debug " - $label_name: ${NEW_LABELS_CREATED[$label_name]}"
- done
- else
- log_debug "NEW_LABELS_CREATED is empty or unset"
- # Debug: Try to list what's in the array anyway
- if [[ "${#NEW_LABELS_CREATED[@]}" -gt 0 ]] 2>/dev/null; then
- for key in "${!NEW_LABELS_CREATED[@]}"; do
- log_debug " Found key: $key"
- done
- else
- log_debug " Array iteration failed - truly empty"
- fi
- fi
-
- if [[ $new_labels_count -eq 0 ]]; then
- log_debug "No new labels created - skipping auto-update"
- return 0 # No new labels, no update needed
- fi
-
- log_info "🔄 Auto-updating scripts with $new_labels_count new labels..."
-
- # Check if update script exists
- local update_script="$SCRIPT_DIR/update_script_labels.sh"
- if [[ ! -f "$update_script" ]]; then
- log_warning "Update script not found: $update_script"
- log_warning "Skipping auto-update"
- return 0
- fi
-
- if [[ ! -x "$update_script" ]]; then
- log_warning "Update script not executable: $update_script"
- log_warning "Making executable..."
- chmod +x "$update_script"
- fi
-
- # Add new labels to registry
- for label_name in "${!NEW_LABELS_CREATED[@]}"; do
- local label_info="${NEW_LABELS_CREATED[$label_name]}"
- local color=$(echo "$label_info" | cut -d':' -f1)
- local context=$(echo "$label_info" | cut -d':' -f2)
- local usage=$(echo "$label_info" | cut -d':' -f3)
-
- log_info "Adding '$label_name' to registry..."
-
- # Add to registry (suppressing output to avoid noise)
- FURT_AUTO_UPDATE=true "$update_script" add "$label_name" "$color" "$context" "$usage" >/dev/null 2>&1 || {
- log_warning "Failed to add $label_name to registry"
- }
- done
-
- # Update all scripts with new labels
- log_info "Synchronizing all scripts..."
- "$update_script" update >/dev/null 2>&1 || {
- log_warning "Failed to update scripts"
- return 0
- }
-
- log_success "✅ All scripts automatically synchronized with new labels!"
-
- # Show what was added
- echo ""
- echo "🆕 New labels created and synchronized:"
- for label_name in "${!NEW_LABELS_CREATED[@]}"; do
- echo " - $label_name (ID: ${LABEL_IDS[$label_name]})"
- done
- echo ""
-}
-
-# Create issue templates
-create_service_issue() {
- local service_name="${1:-newsletter}"
-
- log_debug "Creating service issue for: $service_name"
-
- local title="[SERVICE] $service_name für Furt Gateway"
- local body="# Service-Request: $service_name
-
-## 🏷️ Service-Details
-**Name:** $service_name
-**Port:** TBD
-**Zweck:** [Service-Beschreibung]
-
-## 📝 Funktionsanforderungen
-- [ ] [Anforderung 1]
-- [ ] [Anforderung 2]
-- [ ] [Anforderung 3]
-
-## 🔗 Gateway-Integration
-- [ ] **Routing:** \`/v1/$service_name/*\`
-- [ ] **Auth:** API-Key required
-- [ ] **Rate-Limiting:** TBD req/min
-- [ ] **Health-Check:** \`/health\`
-
-## 🎯 Hugo-Integration
-- [ ] **Shortcode:** \`{{< furt-$service_name >}}\`
-- [ ] **JavaScript-Client**
-- [ ] **CSS-Styling**
-
-## ⚡ Priorität
-🔥 **Hoch** - benötigt für Website-Launch"
-
- # Process labels first, then build JSON
- local service_label="service-$service_name"
- log_debug "Service label to add: $service_label"
-
- # First: Process all labels (this updates global arrays)
- process_labels_for_template "service" "$service_label"
-
- # Then: Build JSON from already-processed labels (pure function, no side effects)
- local labels_json=$(build_labels_json_from_processed "service" "$service_label")
-
- # Show which labels are being used (AFTER processing when labels are actually created)
- show_labels_used "service" "$service_label"
-
- create_issue "$title" "$body" "$labels_json"
-}
-
-create_architecture_issue() {
- local topic="${1:-middleware-optimization}"
-
- local title="[ARCH] Gateway $topic"
- local body="# Architektur-Diskussion: $topic
-
-## 🎯 Architektur-Thema
-[Beschreibung des Architektur-Themas]
-
-## 📊 Aktuelle Situation
-- [Status Quo 1]
-- [Status Quo 2]
-
-## 💡 Vorgeschlagene Änderung
-- [Vorschlag 1]
-- [Vorschlag 2]
-
-## 🔄 Alternativen
-1. **Option A:** [Beschreibung]
-2. **Option B:** [Beschreibung]
-
-## 📈 Betroffene Bereiche
-- [ ] Gateway-Performance
-- [ ] Service-Integration
-- [ ] Security
-- [ ] Configuration-Management"
-
- # Process labels first, then build JSON
- process_labels_for_template "architecture"
- local labels_json=$(build_labels_json_from_processed "architecture")
-
- create_issue "$title" "$body" "$labels_json"
-}
-
-# Generic template creator
-create_generic_issue() {
- local template="$1"
- local component="${2:-gateway}"
- local description="[Beschreibung hinzufügen]"
-
- # Safe parameter handling for $3
- if [[ $# -ge 3 ]] && [[ -n "${3:-}" ]]; then
- description="$3"
- fi
-
- log_debug "Creating $template issue for: $component"
-
- local title_prefix
- case "$template" in
- api) title_prefix="[API]" ;;
- security) title_prefix="[SEC]" ;;
- hugo) title_prefix="[HUGO]" ;;
- deployment) title_prefix="[DEPLOY]" ;;
- bug) title_prefix="[BUG]" ;;
- *) title_prefix="[${template^^}]" ;;
- esac
-
- local title="$title_prefix $component $(echo ${template^} | sed 's/api/API Contract/')"
- local body="# ${template^}: $component
-
-## 📝 ${template^}-Details
-**Komponente:** $component
-**Beschreibung:** $description
-
-## 🎯 Anforderungen
-- [ ] [Anforderung 1]
-- [ ] [Anforderung 2]
-- [ ] [Anforderung 3]
-
-## ✅ Definition of Done
-- [ ] [DoD Kriterium 1]
-- [ ] [DoD Kriterium 2]
-- [ ] [DoD Kriterium 3]"
-
- # Process labels first, then build JSON
- process_labels_for_template "$template"
- local labels_json=$(build_labels_json_from_processed "$template")
-
- show_labels_used "$template"
- create_issue "$title" "$body" "$labels_json"
-}
-
-# Show usage information
-show_usage() {
- echo "🎯 Furt API-Gateway Issue Creator (Debug Version)"
- echo ""
- echo "Usage: $0 [TEMPLATE] [OPTIONS]"
- echo ""
- echo "📋 Available Templates:"
- echo " service [name] New service for gateway (default: newsletter)"
- echo " architecture [topic] Gateway architecture discussion (default: middleware-optimization)"
- echo " performance [comp] Performance optimization (default: gateway)"
- echo " api [service] API contract update (default: formular2mail)"
- echo " security [comp] Security review/issue (default: gateway)"
- echo " bug [comp] [desc] Bug report (default: gateway)"
- echo " hugo [feature] Hugo integration (default: shortcode)"
- echo " deployment [comp] Deployment issue (default: gateway)"
- echo " custom Custom issue (interactive)"
- echo ""
- echo "🚀 Examples:"
- echo " $0 service newsletter # Create newsletter service request"
- echo " $0 architecture rate-limiting # Discuss rate limiting architecture"
- echo " $0 performance gateway # Gateway performance optimization"
- echo " $0 custom # Interactive custom issue"
- echo ""
- echo "🔧 Debug Mode:"
- echo " Set DEBUG=1 for verbose debug output"
-}
-
-# Generic issue creation
-create_issue() {
- local title="$1"
- local body="$2"
- local labels_json="$3"
-
- log_info "Creating issue: $title"
- log_debug "Labels JSON: $labels_json"
-
- local response=$(curl -s -w "\n%{http_code}" -X POST \
- "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues" \
- -H "Authorization: token $GITEA_TOKEN" \
- -H "Content-Type: application/json" \
- -d "{
- \"title\": $(echo "$title" | jq -R .),
- \"body\": $(echo "$body" | jq -R -s .),
- \"labels\": $labels_json
- }")
-
- local http_code=$(echo "$response" | tail -n1)
- local response_body=$(echo "$response" | head -n -1)
-
- if [[ "$http_code" == "201" ]]; then
- local issue_number=$(echo "$response_body" | jq -r '.number')
- local issue_url=$(echo "$response_body" | jq -r '.html_url')
-
- log_success "Issue #$issue_number created!"
- echo "🔗 $issue_url"
- else
- log_error "Failed to create issue (HTTP: $http_code)"
- log_error "Response: $response_body"
- exit 1
- fi
-}
-
-# Main function
-main() {
- local template="${1:-help}"
-
- # Enable debug if requested
- if [[ "${DEBUG:-}" == "1" ]]; then
- log_info "Debug mode enabled"
- fi
-
- if [[ "$template" == "help" ]] || [[ "$template" == "--help" ]] || [[ "$template" == "-h" ]]; then
- show_usage
- exit 0
- fi
-
- # Load existing labels
- load_existing_labels
-
- case "$template" in
- service)
- create_service_issue "${2:-newsletter}"
- ;;
- architecture)
- create_architecture_issue "${2:-middleware-optimization}"
- ;;
- performance)
- local component="${2:-gateway}"
- local title="[PERF] $component Performance-Optimierung"
- local body="# Performance-Optimierung: $component"
-
- process_labels_for_template "performance"
- local labels_json=$(build_labels_json_from_processed "performance")
-
- create_issue "$title" "$body" "$labels_json"
- ;;
- api|security|hugo|deployment|bug)
- if [[ $# -ge 3 ]]; then
- create_generic_issue "$template" "${2:-gateway}" "$3"
- else
- create_generic_issue "$template" "${2:-gateway}"
- fi
- ;;
- *)
- log_error "Unknown template: $template"
- show_usage
- exit 1
- ;;
- esac
-
- # FIXED: AUTO-UPDATE: Automatically sync scripts if new labels were created
- auto_update_scripts_if_needed
-}
-
-# Run if executed directly
-if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
- main "$@"
-fi
-
diff --git a/scripts/archive/get_issues_v1.sh b/scripts/archive/get_issues_v1.sh
deleted file mode 100755
index ae8f0c9..0000000
--- a/scripts/archive/get_issues_v1.sh
+++ /dev/null
@@ -1,250 +0,0 @@
-#!/bin/bash
-
-# Load environment
-if [ -f .env ]; then
- export $(cat .env | grep -v '^#' | xargs)
-else
- echo "❌ .env file not found!"
- echo "📋 Copy .env.example to .env and configure it first"
- exit 1
-fi
-
-# Validate required variables
-if [ -z "$GITEA_URL" ] || [ -z "$REPO_OWNER" ] || [ -z "$REPO_NAME" ] || [ -z "$GITEA_TOKEN" ]; then
- echo "❌ Missing required environment variables in .env"
- exit 1
-fi
-
-# Colors
-GREEN='\033[0;32m'
-BLUE='\033[0;34m'
-YELLOW='\033[1;33m'
-RED='\033[0;31m'
-NC='\033[0m'
-
-log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
-log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
-log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
-
-# Get all issues with nice formatting
-get_all_issues() {
- log_info "Fetching all issues..."
- echo ""
-
- response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues" \
- -H "Authorization: token $GITEA_TOKEN")
-
- if [ $? -ne 0 ]; then
- echo "❌ Error fetching issues"
- return 1
- fi
-
- echo "$response" | jq -r '.[] |
- "🎯 #\(.number) \(.title)",
- " 📊 State: \(.state) | 🏷️ Labels: \(.labels | map(.name) | join(", ") // "none")",
- " 🔗 \(.html_url)",
- ""'
-}
-
-# Get issues by label
-get_issues_by_label() {
- local label="$1"
- log_info "Fetching issues with label: $label"
- echo ""
-
- response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues?labels=$label" \
- -H "Authorization: token $GITEA_TOKEN")
-
- echo "$response" | jq -r '.[] |
- "🎯 #\(.number) \(.title)",
- " 📊 \(.state) | 🔗 \(.html_url)",
- ""'
-}
-
-# Get issue details
-get_issue_details() {
- local issue_number="$1"
- log_info "Fetching details for issue #$issue_number"
- echo ""
-
- response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues/$issue_number" \
- -H "Authorization: token $GITEA_TOKEN")
-
- echo "$response" | jq -r '
- "🎯 Issue #\(.number): \(.title)",
- "📊 State: \(.state)",
- "👤 Assignees: \(.assignees | map(.login) | join(", ") // "none")",
- "🏷️ Labels: \(.labels | map(.name) | join(", ") // "none")",
- "📅 Created: \(.created_at)",
- "🔗 URL: \(.html_url)",
- "",
- "📝 Body:",
- "\(.body // "No description")",
- ""'
-}
-
-# Close issue
-close_issue() {
- local issue_number="$1"
- log_info "Closing issue #$issue_number"
-
- response=$(curl -s -w "\n%{http_code}" -X PATCH \
- "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues/$issue_number" \
- -H "Authorization: token $GITEA_TOKEN" \
- -H "Content-Type: application/json" \
- -d '{"state": "closed"}')
-
- http_code=$(echo "$response" | tail -n1)
- if [ "$http_code" = "201" ]; then
- log_success "Issue #$issue_number closed"
- else
- echo "❌ Failed to close issue (HTTP: $http_code)"
- fi
-}
-
-# Get pipeline status (issues grouped by Kanban columns for Furt)
-get_pipeline_status() {
- log_info "Furt API Gateway - Pipeline Status Overview"
- echo ""
-
- echo "🔧 SERVICE REQUESTS:"
- get_issues_by_label "service-request" | head -10
-
- echo "🏗️ ARCHITECTURE DISCUSSIONS:"
- get_issues_by_label "architecture"
-
- echo "🚀 PERFORMANCE OPTIMIZATIONS:"
- get_issues_by_label "performance"
-
- echo "🔒 SECURITY REVIEWS:"
- get_issues_by_label "security"
-
- echo "🐛 BUGS:"
- get_issues_by_label "bug"
-
- echo "🌐 HUGO INTEGRATIONS:"
- get_issues_by_label "hugo-integration"
-
- echo "📋 WORK IN PROGRESS:"
- get_issues_by_label "enhancement" | head -5
-}
-
-# Issue statistics
-get_stats() {
- log_info "Furt API Gateway - Issue Statistics"
- echo ""
-
- all_issues=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues" \
- -H "Authorization: token $GITEA_TOKEN")
-
- total=$(echo "$all_issues" | jq length)
- open=$(echo "$all_issues" | jq '[.[] | select(.state == "open")] | length')
- closed=$(echo "$all_issues" | jq '[.[] | select(.state == "closed")] | length')
-
- echo "📊 Total Issues: $total"
- echo "✅ Open: $open"
- echo "🔒 Closed: $closed"
- echo ""
-
- echo "🏷️ Furt Labels:"
- echo "$all_issues" | jq -r '[.[] | .labels[].name] | group_by(.) | map({label: .[0], count: length}) | sort_by(.count) | reverse | limit(10; .[]) | " \(.label): \(.count)"'
-}
-
-case "${1:-help}" in
- "all"|"")
- get_all_issues
- ;;
- "gateway")
- get_issues_by_label "gateway"
- ;;
- "service-request")
- get_issues_by_label "service-request"
- ;;
- "service-formular2mail")
- get_issues_by_label "service-formular2mail"
- ;;
- "service-sagjan")
- get_issues_by_label "service-sagjan"
- ;;
- "architecture")
- get_issues_by_label "architecture"
- ;;
- "performance")
- get_issues_by_label "performance"
- ;;
- "security")
- get_issues_by_label "security"
- ;;
- "bug")
- get_issues_by_label "bug"
- ;;
- "enhancement")
- get_issues_by_label "enhancement"
- ;;
- "hugo")
- get_issues_by_label "hugo-integration"
- ;;
- "deployment")
- get_issues_by_label "deployment"
- ;;
- "testing")
- get_issues_by_label "testing"
- ;;
- "documentation")
- get_issues_by_label "documentation"
- ;;
- "pipeline")
- get_pipeline_status
- ;;
- "stats")
- get_stats
- ;;
- "close")
- if [ -z "$2" ]; then
- echo "Usage: $0 close ISSUE_NUMBER"
- exit 1
- fi
- close_issue "$2"
- ;;
- [0-9]*)
- get_issue_details "$1"
- ;;
- *)
- echo "🎯 Furt API Gateway - Issues Manager"
- echo ""
- echo "Usage: $0 [COMMAND] [OPTIONS]"
- echo ""
- echo "📋 List Commands:"
- echo " all List all issues (default)"
- echo " gateway Gateway core issues"
- echo " service-request New service requests"
- echo " service-formular2mail Formular2mail service issues"
- echo " service-sagjan Sagjan service issues"
- echo " architecture Architecture discussions"
- echo " performance Performance optimizations"
- echo " security Security reviews"
- echo " bug Bug reports"
- echo " enhancement New features"
- echo " hugo Hugo integration issues"
- echo " deployment Deployment issues"
- echo " testing Testing issues"
- echo " documentation Documentation updates"
- echo ""
- echo "📊 Analysis Commands:"
- echo " pipeline Kanban pipeline status"
- echo " stats Issue statistics"
- echo ""
- echo "⚙️ Management Commands:"
- echo " close NUM Close issue #NUM"
- echo " NUM Show details for issue #NUM"
- echo ""
- echo "🚀 Examples:"
- echo " $0 # List all issues"
- echo " $0 pipeline # Show pipeline status"
- echo " $0 service-request # Show service requests"
- echo " $0 gateway # Show gateway issues"
- echo " $0 5 # Show issue #5 details"
- echo " $0 close 3 # Close issue #3"
- ;;
-esac
-
diff --git a/scripts/archive/update_issue_v1.sh b/scripts/archive/update_issue_v1.sh
deleted file mode 100755
index b7c813c..0000000
--- a/scripts/archive/update_issue_v1.sh
+++ /dev/null
@@ -1,156 +0,0 @@
-#!/bin/bash
-
-# Load environment
-if [ -f .env ]; then
- export $(cat .env | grep -v '^#' | xargs)
-else
- echo "❌ .env file not found!"
- exit 1
-fi
-
-# Colors
-GREEN='\033[0;32m'
-BLUE='\033[0;34m'
-RED='\033[0;31m'
-NC='\033[0m'
-
-log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
-log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
-log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
-
-# Get all labels with IDs
-declare -A LABEL_IDS
-get_labels() {
- response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/labels" \
- -H "Authorization: token $GITEA_TOKEN")
- while IFS= read -r line; do
- name=$(echo "$line" | jq -r '.name')
- id=$(echo "$line" | jq -r '.id')
- LABEL_IDS["$name"]="$id"
- done < <(echo "$response" | jq -c '.[]')
-}
-
-# Add comment to issue
-add_comment() {
- local issue_number="$1"
- local comment="$2"
-
- # Use jq for proper JSON escaping
- local json_payload=$(jq -n --arg body "$comment" '{body: $body}')
-
- response=$(curl -s -w "\n%{http_code}" -X POST \
- "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues/$issue_number/comments" \
- -H "Authorization: token $GITEA_TOKEN" \
- -H "Content-Type: application/json" \
- -d "$json_payload")
-
- http_code=$(echo "$response" | tail -n1)
- if [ "$http_code" = "201" ]; then
- log_success "Comment added to issue #$issue_number"
- else
- log_error "Failed to add comment (HTTP: $http_code)"
- fi
-}
-
-# Update issue labels - FIXED VERSION
-update_labels() {
- local issue_number="$1"
- local labels_string="$2"
-
- get_labels
-
- # Convert to ID array
- local valid_label_ids=()
- IFS=',' read -ra LABEL_ARRAY <<< "$labels_string"
-
- for label in "${LABEL_ARRAY[@]}"; do
- label=$(echo "$label" | xargs)
- if [ -n "${LABEL_IDS[$label]}" ]; then
- valid_label_ids+=("${LABEL_IDS[$label]}")
- else
- log_error "Label '$label' not found!"
- return 1
- fi
- done
-
- # Build ID array JSON
- local labels_json="["
- for i in "${!valid_label_ids[@]}"; do
- if [ $i -gt 0 ]; then
- labels_json="${labels_json},"
- fi
- labels_json="${labels_json}${valid_label_ids[$i]}"
- done
- labels_json="${labels_json}]"
-
- response=$(curl -s -w "\n%{http_code}" -X PUT \
- "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues/$issue_number/labels" \
- -H "Authorization: token $GITEA_TOKEN" \
- -H "Content-Type: application/json" \
- -d "$labels_json")
-
- http_code=$(echo "$response" | tail -n1)
- if [ "$http_code" = "200" ]; then
- log_success "Labels updated for issue #$issue_number"
- else
- log_error "Failed to update labels (HTTP: $http_code)"
- fi
-}
-
-case "${1:-help}" in
- "comment")
- if [ -z "$2" ] || [ -z "$3" ]; then
- echo "Usage: $0 comment ISSUE_NUMBER \"COMMENT_TEXT\""
- exit 1
- fi
- add_comment "$2" "$3"
- ;;
- "labels")
- if [ -z "$2" ] || [ -z "$3" ]; then
- echo "Usage: $0 labels ISSUE_NUMBER \"label1,label2,label3\""
- exit 1
- fi
- update_labels "$2" "$3"
- ;;
- "progress")
- if [ -z "$2" ]; then
- echo "Usage: $0 progress ISSUE_NUMBER"
- exit 1
- fi
- add_comment "$2" "📊 **Progress Update:** Arbeit an dieser Analyse läuft. Erste Quellen werden gesammelt und Framework-Relevanz geprüft."
- update_labels "$2" "work-in-progress"
- ;;
- "review")
- if [ -z "$2" ]; then
- echo "Usage: $0 review ISSUE_NUMBER"
- exit 1
- fi
- add_comment "$2" "👀 **Ready for Review:** Erste Analyse abgeschlossen. Bitte um Peer-Review der Quellen und Framework-Integration."
- update_labels "$2" "needs-review"
- ;;
- "fact-check")
- if [ -z "$2" ]; then
- echo "Usage: $0 fact-check ISSUE_NUMBER"
- exit 1
- fi
- add_comment "$2" "🔍 **Fact-Check Required:** Kritische Behauptungen gefunden die zusätzliche Quellen-Verifikation benötigen."
- update_labels "$2" "fact-check-needed"
- ;;
- *)
- echo "🔧 Issue Update Tool (FIXED VERSION)"
- echo ""
- echo "Usage: $0 COMMAND ISSUE_NUMBER [OPTIONS]"
- echo ""
- echo "Commands:"
- echo " comment NUM \"TEXT\" Add comment to issue"
- echo " labels NUM \"l1,l2\" Update issue labels (using IDs)"
- echo " progress NUM Mark as work-in-progress"
- echo " review NUM Mark as ready for review"
- echo " fact-check NUM Mark as needing fact-check"
- echo ""
- echo "Examples:"
- echo " $0 comment 5 \"Erste Quellen gefunden\""
- echo " $0 labels 3 \"regional-case,work-in-progress\""
- echo " $0 progress 7"
- ;;
-esac
diff --git a/scripts/archive/update_script_labels_monster.sh b/scripts/archive/update_script_labels_monster.sh
deleted file mode 100755
index 757c291..0000000
--- a/scripts/archive/update_script_labels_monster.sh
+++ /dev/null
@@ -1,491 +0,0 @@
-#!/bin/bash
-# scripts/update_script_labels.sh
-# Auto-updates all scripts with current label definitions from registry
-# FINAL FIXED VERSION with corrected all_templates logic
-
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
-REGISTRY_FILE="$PROJECT_ROOT/configs/labels.registry"
-
-# Colors for logging
-GREEN='\033[0;32m'
-BLUE='\033[0;34m'
-RED='\033[0;31m'
-YELLOW='\033[1;33m'
-NC='\033[0m'
-
-log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
-log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
-log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
-log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
-
-# Parse label registry into associative arrays
-declare -A LABEL_COLORS
-declare -A LABEL_CONTEXTS
-declare -A LABEL_USAGES
-
-# Create registry file if it doesn't exist
-create_registry_if_missing() {
- if [[ ! -f "$REGISTRY_FILE" ]]; then
- log_info "Creating label registry file..."
-
- mkdir -p "$(dirname "$REGISTRY_FILE")"
-
- cat > "$REGISTRY_FILE" << 'EOF'
-# Central Label Registry for Furt API Gateway Project
-# Format: name:color:context:usage_contexts
-# This file is the single source of truth for all labels
-
-# === CORE WORKFLOW LABELS ===
-service-request:7057ff:new_service:service_templates,status_updates
-enhancement:84b6eb:improvement:all_templates
-bug:d73a4a:error:bug_template,status_updates
-question:d876e3:discussion:question_template
-
-# === COMPONENT CATEGORIES ===
-gateway:0052cc:gateway_core:architecture_template,performance_template,service_templates
-performance:fbca04:optimization:performance_template,architecture_template
-architecture:d4c5f9:design:architecture_template,gateway
-security:28a745:security_review:security_template,architecture_template
-configuration:f9d71c:config_management:deployment_template,architecture_template
-
-# === SERVICE-SPECIFIC LABELS ===
-service-formular2mail:1d76db:formular2mail:formular2mail_integration
-service-sagjan:1d76db:sagjan:sagjan_integration
-service-newsletter:ff6b6b:newsletter:newsletter_integration
-
-# === WORKFLOW STATE LABELS ===
-work-in-progress:fbca04:active:status_updates
-needs-review:0e8a16:review:status_updates
-blocked:d73a4a:blocked:status_updates
-ready-for-deployment:28a745:deploy_ready:status_updates
-
-# === INTEGRATION LABELS ===
-hugo-integration:ff7518:frontend:hugo_templates,integration
-api-contract:5319e7:api_design:api_templates,service_templates
-breaking-change:d73a4a:breaking:api_templates,architecture_template
-
-# === PRIORITY LABELS ===
-high-priority:d73a4a:urgent:all_templates
-low-priority:0e8a16:nice_to_have:all_templates
-
-# === META LABELS ===
-low-tech:6f42c1:low_tech_principle:architecture_template,performance_template,security_template
-digital-sovereignty:6f42c1:digital_sovereignty:architecture_template,performance_template,security_template
-good-first-issue:7057ff:beginner_friendly:manual_assignment
-help-wanted:159818:community_help:manual_assignment
-
-# === DEPLOYMENT LABELS ===
-deployment:ff7518:deployment:deployment_template
-testing:f9d71c:testing:testing_template,integration
-EOF
-
- log_success "Created label registry: $REGISTRY_FILE"
- fi
-}
-
-parse_registry() {
- create_registry_if_missing
-
- log_info "Parsing label registry..."
-
- while IFS= read -r line; do
- # Skip comments and empty lines
- [[ "$line" =~ ^#.*$ ]] && continue
- [[ -z "$line" ]] && continue
-
- # Parse format: name:color:context:usage_contexts
- if [[ "$line" =~ ^([^:]+):([^:]+):([^:]+):(.+)$ ]]; then
- local name="${BASH_REMATCH[1]}"
- local color="${BASH_REMATCH[2]}"
- local context="${BASH_REMATCH[3]}"
- local usage="${BASH_REMATCH[4]}"
-
- LABEL_COLORS["$name"]="$color"
- LABEL_CONTEXTS["$name"]="$context"
- LABEL_USAGES["$name"]="$usage"
-
- log_info "Loaded label: $name ($context)"
- fi
- done < "$REGISTRY_FILE"
-
- log_success "Loaded ${#LABEL_COLORS[@]} labels from registry"
-}
-
-# Generate label definitions section for scripts
-generate_label_definitions() {
- cat << 'EOF'
-# === LABEL DEFINITIONS START ===
-# This section is auto-maintained by update_script_labels.sh
-# DO NOT EDIT MANUALLY - Changes will be overwritten
-declare -A LABEL_DEFINITIONS=(
-EOF
-
- for label in "${!LABEL_COLORS[@]}"; do
- local color="${LABEL_COLORS[$label]}"
- local context="${LABEL_CONTEXTS[$label]}"
- local usage="${LABEL_USAGES[$label]}"
-
- echo " [\"$label\"]=\"color:$color;context:$context;usage:$usage\""
- done
-
- cat << 'EOF'
-)
-
-# Extract label info
-get_label_color() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f1 | cut -d':' -f2; }
-get_label_context() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f2 | cut -d':' -f2; }
-get_label_usage() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f3 | cut -d':' -f2; }
-
-# Check if label is valid for context
-is_label_valid_for_context() {
- local label="$1"
- local context="$2"
- local usage=$(get_label_usage "$label")
- [[ "$usage" == *"$context"* ]] || [[ "$usage" == "all_templates" ]]
-}
-# === LABEL DEFINITIONS END ===
-EOF
-}
-
-# Generate template-to-labels mapping - FIXED VERSION
-generate_template_mappings() {
- cat << 'EOF'
-
-# === TEMPLATE LABEL MAPPINGS START ===
-# Auto-generated template to label mappings
-declare -A TEMPLATE_LABELS=(
-EOF
-
- # FIXED: Consistent template names with corrected all_templates logic
- declare -A template_mappings=(
- ["service"]="service_templates"
- ["architecture"]="architecture_template"
- ["performance"]="performance_template"
- ["bug"]="bug_template"
- ["security"]="security_template"
- ["hugo"]="hugo_templates"
- ["api"]="api_templates"
- ["deployment"]="deployment_template"
- )
-
- for template_name in "${!template_mappings[@]}"; do
- local template_usage="${template_mappings[$template_name]}"
- local labels=()
-
- # FIXED: First add all_templates labels to every template
- for label in "${!LABEL_USAGES[@]}"; do
- local usage="${LABEL_USAGES[$label]}"
-
- # EXCLUDE service-specific labels from all templates
- if [[ "$label" == service-* ]]; then
- continue
- fi
-
- # Skip manual assignment labels
- if [[ "$usage" == "manual_assignment" ]]; then
- continue
- fi
-
- # FIXED: Add all_templates labels to every template first
- if [[ "$usage" == "all_templates" ]]; then
- labels+=("$label")
- continue
- fi
-
- # Add template-specific labels
- if [[ "$usage" == *"$template_usage"* ]]; then
- labels+=("$label")
- fi
- done
-
- if [[ ${#labels[@]} -gt 0 ]]; then
- local label_list=$(IFS=','; echo "${labels[*]}")
- echo " [\"$template_name\"]=\"$label_list\""
- fi
- done
-
- cat << 'EOF'
-)
-# === TEMPLATE LABEL MAPPINGS END ===
-EOF
-}
-
-# Generate filter options for get_issues.sh
-generate_filter_options() {
- cat << 'EOF'
-
-# === FILTER OPTIONS START ===
-# Auto-generated filter options for get_issues.sh
-show_filter_help() {
- echo "📋 Available filters:"
-EOF
-
- # Group labels by context for better help display
- declare -A context_labels
- for label in "${!LABEL_CONTEXTS[@]}"; do
- local context="${LABEL_CONTEXTS[$label]}"
- if [[ -z "${context_labels[$context]}" ]]; then
- context_labels[$context]="$label"
- else
- context_labels[$context]="${context_labels[$context]},$label"
- fi
- done
-
- for context in "${!context_labels[@]}"; do
- echo " echo \" $context: ${context_labels[$context]}\""
- done
-
- cat << 'EOF'
-}
-
-# Filter case statement
-handle_filter() {
- local filter="$1"
- case "$filter" in
-EOF
-
- for label in "${!LABEL_COLORS[@]}"; do
- echo " $label) filter_by_label \"$label\" ;;"
- done
-
- cat << 'EOF'
- pipeline) show_pipeline_overview ;;
- stats) show_statistics ;;
- all) show_all_issues ;;
- *)
- log_error "Unknown filter: $filter"
- show_filter_help
- exit 1
- ;;
- esac
-}
-# === FILTER OPTIONS END ===
-EOF
-}
-
-# Update a single script file
-update_script_file() {
- local script_file="$1"
-
- if [[ ! -f "$script_file" ]]; then
- log_warning "Script not found: $script_file"
- return 1
- fi
-
- log_info "Updating $script_file..."
-
- # Create backup
- cp "$script_file" "${script_file}.backup"
-
- # Check if script has label definition sections
- if ! grep -q "# === LABEL DEFINITIONS START ===" "$script_file"; then
- log_warning "$script_file has no label definitions section - skipping"
- return 0
- fi
-
- # Find section boundaries
- local start_line=$(grep -n "# === LABEL DEFINITIONS START ===" "$script_file" | cut -d: -f1)
- local end_line=$(grep -n "# === LABEL DEFINITIONS END ===" "$script_file" | cut -d: -f1)
-
- if [[ -z "$start_line" ]] || [[ -z "$end_line" ]]; then
- log_error "Malformed label definition section in $script_file"
- return 1
- fi
-
- # Generate new content
- local new_definitions=$(generate_label_definitions)
-
- # Handle template mappings if present
- if grep -q "# === TEMPLATE LABEL MAPPINGS START ===" "$script_file"; then
- new_definitions+="\n$(generate_template_mappings)"
- fi
-
- # Handle filter options if present (for get_issues.sh)
- if grep -q "# === FILTER OPTIONS START ===" "$script_file"; then
- new_definitions+="\n$(generate_filter_options)"
- fi
-
- # Create temporary file with updated content
- local temp_file=$(mktemp)
-
- # Copy everything before label definitions
- sed -n "1,$((start_line-1))p" "$script_file" > "$temp_file"
-
- # Add new definitions
- echo -e "$new_definitions" >> "$temp_file"
-
- # Copy everything after label definitions (find new end line)
- if grep -q "# === TEMPLATE LABEL MAPPINGS END ===" "$script_file"; then
- local actual_end_line=$(grep -n "# === TEMPLATE LABEL MAPPINGS END ===" "$script_file" | cut -d: -f1)
- elif grep -q "# === FILTER OPTIONS END ===" "$script_file"; then
- local actual_end_line=$(grep -n "# === FILTER OPTIONS END ===" "$script_file" | cut -d: -f1)
- else
- local actual_end_line="$end_line"
- fi
-
- sed -n "$((actual_end_line+1)),\$p" "$script_file" >> "$temp_file"
-
- # Replace original file
- mv "$temp_file" "$script_file"
- chmod +x "$script_file"
-
- log_success "Updated $script_file"
-}
-
-# Add new label to registry
-add_label_to_registry() {
- local name="$1"
- local color="${2:-ff6b6b}"
- local context="${3:-auto_generated}"
- local usage="${4:-manual_assignment}"
-
- # Skip if called during auto-update to prevent loops
- if [[ "${FURT_AUTO_UPDATE:-}" == "true" ]]; then
- log_info "Auto-update mode: Adding $name to registry (skipping rebuild)"
- else
- log_info "Adding new label to registry: $name"
- fi
-
- # Ensure registry exists
- create_registry_if_missing
-
- # Check if label already exists
- if grep -q "^$name:" "$REGISTRY_FILE"; then
- log_warning "Label $name already exists in registry"
- return 0
- fi
-
- # Add to appropriate section (determine by context)
- local section_marker="# === CORE WORKFLOW LABELS ==="
- if [[ "$context" == *"service"* ]] || [[ "$name" == service-* ]]; then
- section_marker="# === SERVICE-SPECIFIC LABELS ==="
- elif [[ "$context" == *"workflow"* ]]; then
- section_marker="# === WORKFLOW STATE LABELS ==="
- elif [[ "$context" == *"component"* ]]; then
- section_marker="# === COMPONENT CATEGORIES ==="
- fi
-
- # Create backup
- cp "$REGISTRY_FILE" "${REGISTRY_FILE}.backup"
-
- # Find section and add label
- local temp_file=$(mktemp)
- local added=false
-
- while IFS= read -r line; do
- echo "$line" >> "$temp_file"
-
- if [[ "$line" == "$section_marker" ]] && [[ "$added" == false ]]; then
- echo "$name:$color:$context:$usage" >> "$temp_file"
- added=true
- log_success "Added label $name to registry"
- fi
- done < "$REGISTRY_FILE"
-
- mv "$temp_file" "$REGISTRY_FILE"
-}
-
-# Show current registry status
-show_registry_status() {
- echo "📊 Label Registry Status"
- echo "========================"
- echo "Registry file: $REGISTRY_FILE"
-
- if [[ -f "$REGISTRY_FILE" ]]; then
- echo "Total labels: $(grep -c "^[^#]" "$REGISTRY_FILE" 2>/dev/null || echo 0)"
- echo ""
- echo "Labels by category:"
-
- local current_section=""
- while IFS= read -r line; do
- if [[ "$line" =~ ^#\ ===.*===\ $ ]]; then
- current_section=$(echo "$line" | sed 's/# === \(.*\) ===/\1/')
- echo " $current_section:"
- elif [[ "$line" =~ ^[^#]+: ]] && [[ -n "$current_section" ]]; then
- local label_name=$(echo "$line" | cut -d: -f1)
- echo " - $label_name"
- fi
- done < "$REGISTRY_FILE"
- else
- echo "Registry file not found!"
- fi
-}
-
-# Main function
-main() {
- local command="${1:-update}"
-
- case "$command" in
- update)
- log_info "Starting label synchronization..."
- parse_registry
-
- # Update all script files
- local scripts=(
- "$PROJECT_ROOT/scripts/create_issue.sh"
- "$PROJECT_ROOT/scripts/get_issues.sh"
- "$PROJECT_ROOT/scripts/update_issue.sh"
- )
-
- for script in "${scripts[@]}"; do
- update_script_file "$script"
- done
-
- log_success "All scripts synchronized with label registry!"
- ;;
-
- add)
- local name="$2"
- local color="${3:-ff6b6b}"
- local context="${4:-auto_generated}"
- local usage="${5:-manual_assignment}"
-
- if [[ -z "$name" ]]; then
- log_error "Usage: $0 add [color] [context] [usage]"
- exit 1
- fi
-
- add_label_to_registry "$name" "$color" "$context" "$usage"
-
- # Only update scripts if not in auto-update mode
- if [[ "${FURT_AUTO_UPDATE:-}" != "true" ]]; then
- parse_registry
- main update
- fi
- ;;
-
- status)
- show_registry_status
- ;;
-
- help)
- echo "Usage: $0 [command] [options]"
- echo ""
- echo "Commands:"
- echo " update Update all scripts with current registry"
- echo " add Add new label to registry and update scripts"
- echo " status Show registry status"
- echo " help Show this help"
- echo ""
- echo "Examples:"
- echo " $0 update"
- echo " $0 add newsletter ff6b6b newsletter_service service_templates"
- echo " $0 status"
- ;;
-
- *)
- log_error "Unknown command: $command"
- main help
- exit 1
- ;;
- esac
-}
-
-# Run if executed directly
-if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
- main "$@"
-fi
-
diff --git a/scripts/build-package.sh b/scripts/build-package.sh
new file mode 100755
index 0000000..e0b3434
--- /dev/null
+++ b/scripts/build-package.sh
@@ -0,0 +1,168 @@
+#!/bin/bash
+# scripts/build-package.sh
+# Clean package creation for furt API Gateway
+# Creates distribution-ready packages excluding development files
+
+set -euo pipefail
+
+# Colors für Output (nur ASCII)
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+NC='\033[0m'
+
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[OK]${NC} $1"
+}
+
+log_warn() {
+ echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Version bestimmen
+get_version() {
+ local version=""
+
+ if [[ $# -gt 0 ]]; then
+ version="$1"
+ elif [[ -f "VERSION" ]]; then
+ version=$(cat VERSION | tr -d '\n\r' | sed 's/^v//')
+ elif git rev-parse --git-dir >/dev/null 2>&1; then
+ version=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')
+ if [[ -z "$version" ]]; then
+ version="0.1.0-$(git rev-parse --short HEAD)"
+ fi
+ else
+ version="0.1.0-$(date +%Y%m%d)"
+ fi
+
+ echo "$version"
+}
+
+# Validiere Projekt-Verzeichnis
+validate_project() {
+ if [[ ! -d "src" ]] || [[ ! -f "src/main.lua" ]]; then
+ log_error "Nicht im furt-Projektverzeichnis oder src/main.lua fehlt"
+ exit 1
+ fi
+
+ if [[ ! -d "config" ]]; then
+ log_error "config/ Verzeichnis fehlt"
+ exit 1
+ fi
+
+ log_success "Projekt-Struktur validiert"
+}
+
+# Erstelle sauberes Paket mit VCS-Detection
+create_package() {
+ local version="$1"
+ local package_name="furt-api-gateway-v${version}.tar.gz"
+
+ log_info "Erstelle Paket: $package_name"
+
+ # Erstelle dist/ Verzeichnis falls nicht vorhanden
+ mkdir -p dist/
+
+ # VCS-Detection für saubere Archive (wie bei merkwerk)
+ if git rev-parse --git-dir >/dev/null 2>&1; then
+ log_info "Using git archive..."
+ git archive --format=tar.gz --prefix=furt-api-gateway-v${version}/ HEAD > "dist/$package_name"
+ elif hg root >/dev/null 2>&1; then
+ log_info "Using hg archive..."
+ hg archive -t tgz -p furt-api-gateway-v${version}/ "dist/$package_name"
+ elif bzr info >/dev/null 2>&1; then
+ log_info "Using bzr export..."
+ bzr export --format=tgz "dist/$package_name" --root=furt-api-gateway-v${version}/
+ elif fossil info >/dev/null 2>&1; then
+ log_info "Using fossil tarball..."
+ fossil tarball --name furt-api-gateway-v${version} "dist/$package_name" HEAD
+ else
+ log_info "No VCS detected, using secure tar exclusions..."
+ tar -czf "dist/$package_name" \
+ --exclude='.git*' --exclude='.hg*' --exclude='.bzr*' --exclude='_FOSSIL_*' \
+ --exclude='dist' --exclude='*.tmp' --exclude='*~' \
+ --exclude='.env*' --exclude='*secret*' --exclude='*key*' \
+ --exclude='*.log' --exclude='*.pid' --exclude='.DS_Store' \
+ --exclude='debug.log' --exclude='furt.pid' \
+ --transform="s,^,furt-api-gateway-v${version}/," \
+ .
+ fi
+
+ # VERSION file in Archive aktualisieren falls nötig
+ if [[ ! -f "VERSION" ]]; then
+ log_warn "VERSION file fehlt - wird im Archiv ergänzt"
+ # Temporär entpacken, VERSION hinzufügen, neu packen
+ local temp_dir=$(mktemp -d)
+ tar -xzf "dist/$package_name" -C "$temp_dir"
+ echo "$version" > "$temp_dir/furt-api-gateway-v${version}/VERSION"
+ tar -czf "dist/$package_name" -C "$temp_dir" "furt-api-gateway-v${version}"
+ rm -rf "$temp_dir"
+ log_info "VERSION file im Archiv ergänzt"
+ fi
+
+ # Package-Info
+ local size=$(du -h "dist/$package_name" | cut -f1)
+ log_success "Paket erstellt: dist/$package_name ($size)"
+
+ # Content-Verification
+ log_info "Paket-Inhalt:"
+ tar -tzf "dist/$package_name" | head -20
+ if [[ $(tar -tzf "dist/$package_name" | wc -l) -gt 20 ]]; then
+ log_info " ... und $(( $(tar -tzf "dist/$package_name" | wc -l) - 20 )) weitere Dateien"
+ fi
+}
+
+# Hilfe anzeigen
+show_help() {
+ echo "build-package.sh - Furt Package Builder"
+ echo ""
+ echo "Usage: $0 [VERSION]"
+ echo ""
+ echo "VERSION:"
+ echo " Explicit version string (e.g., 1.0.0)"
+ echo " If not provided, uses VERSION file or git tags"
+ echo ""
+ echo "Examples:"
+ echo " $0 # Auto-detect version"
+ echo " $0 1.0.0 # Explicit version"
+ echo " $0 1.1.0-rc1 # Pre-release"
+ echo ""
+ echo "Output: dist/furt-api-gateway-vVERSION.tar.gz"
+}
+
+# Main
+main() {
+ if [[ $# -gt 0 ]] && [[ "$1" == "-h" || "$1" == "--help" ]]; then
+ show_help
+ exit 0
+ fi
+
+ log_info "Furt Package Builder"
+
+ validate_project
+
+ local version
+ version=$(get_version "$@")
+ log_info "Version: $version"
+
+ create_package "$version"
+
+ log_success "Package build abgeschlossen!"
+ log_info ""
+ log_info "Nächste Schritte:"
+ log_info " 1. Upload: ./scripts/upload-package.sh $version"
+ log_info " 2. Test: tar -tzf dist/furt-api-gateway-v$version.tar.gz"
+}
+
+main "$@"
+
diff --git a/scripts/cleanup_debug.sh b/scripts/cleanup_debug.sh
new file mode 100755
index 0000000..1a79334
--- /dev/null
+++ b/scripts/cleanup_debug.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# furt-lua/scripts/cleanup_debug.sh
+# Clean up debug code and prepare for production
+
+echo "🧹 Cleaning up debug code for production..."
+
+# Remove debug config script
+if [ -f "debug_config.lua" ]; then
+ rm debug_config.lua
+ echo "✅ Removed debug_config.lua"
+fi
+
+# Check for any remaining DEBUG statements
+echo -e "\n🔍 Checking for remaining DEBUG statements:"
+debug_files=$(grep -r "DEBUG:" src/ 2>/dev/null || true)
+if [ -n "$debug_files" ]; then
+ echo "⚠️ Found DEBUG statements:"
+ echo "$debug_files"
+ echo "Please remove these manually!"
+else
+ echo "✅ No DEBUG statements found"
+fi
+
+# Check for any console.log or print statements that might be debug
+echo -e "\n🔍 Checking for debug print statements:"
+print_files=$(grep -r "print(" src/ | grep -v "-- Allow print" | grep -v "print.*error" || true)
+if [ -n "$print_files" ]; then
+ echo "⚠️ Found print statements (review if needed for production):"
+ echo "$print_files"
+else
+ echo "✅ No debug print statements found"
+fi
+
+# Check test endpoint (should be disabled in production)
+echo -e "\n🔍 Checking for test endpoints:"
+test_endpoints=$(grep -r "/test" src/ || true)
+if [ -n "$test_endpoints" ]; then
+ echo "⚠️ Found test endpoints (disable in production):"
+ echo "$test_endpoints"
+else
+ echo "✅ No test endpoints found"
+fi
+
+# Verify API keys are not hardcoded
+echo -e "\n🔍 Checking for hardcoded API keys:"
+hardcoded_keys=$(grep -r "change-me-in-production" config/ src/ || true)
+if [ -n "$hardcoded_keys" ]; then
+ echo "⚠️ Found development API keys (change for production):"
+ echo "$hardcoded_keys"
+else
+ echo "✅ No hardcoded development keys found"
+fi
+
+echo -e "\n✅ Debug cleanup complete!"
+echo "📋 Production checklist:"
+echo " - [ ] Change API keys in .env"
+echo " - [ ] Disable /test endpoint"
+echo " - [ ] Set CORS_ALLOWED_ORIGINS for production"
+echo " - [ ] Configure production SMTP settings"
+echo " - [ ] Review log levels"
+
diff --git a/scripts/create-service.sh b/scripts/create-service.sh
new file mode 100755
index 0000000..eed3ebe
--- /dev/null
+++ b/scripts/create-service.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+# scripts/create-service.sh - Create system service for furt using repository templates
+
+set -e
+
+# Check if we're in furt source directory
+if [ ! -d "deployment" ]; then
+ echo "Error: deployment/ directory not found - not in furt source directory?"
+ exit 1
+fi
+
+if [ "$(uname)" = "OpenBSD" ]; then
+ # Use OpenBSD rc.d template from repository
+ if [ ! -f "deployment/openbsd/rc.d-furt" ]; then
+ echo "Error: deployment/openbsd/rc.d-furt template not found"
+ exit 1
+ fi
+
+ cp deployment/openbsd/rc.d-furt /etc/rc.d/furt
+ chmod +x /etc/rc.d/furt
+ echo "furt_flags=" >> /etc/rc.conf.local
+ rcctl enable furt
+ echo "OpenBSD service created and enabled using repository template"
+
+elif [ "$(uname)" = "Linux" ]; then
+ # Use systemd template from repository
+ if [ ! -f "deployment/linux/furt.service" ]; then
+ echo "Error: deployment/linux/furt.service template not found"
+ exit 1
+ fi
+
+ cp deployment/linux/furt.service /etc/systemd/system/
+ systemctl daemon-reload
+ systemctl enable furt
+ echo "Linux systemd service created and enabled using repository template"
+
+else
+ echo "Unsupported operating system for service creation"
+ exit 1
+fi
+
diff --git a/scripts/health-check.sh b/scripts/health-check.sh
new file mode 100755
index 0000000..c193a84
--- /dev/null
+++ b/scripts/health-check.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+# scripts/health-check.sh - Basic health check for furt service
+
+set -e
+
+# Default values
+HOST="127.0.0.1"
+PORT="7811"
+
+# Parse command line arguments
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --host) HOST="$2"; shift 2 ;;
+ --port) PORT="$2"; shift 2 ;;
+ *) echo "Usage: $0 [--host HOST] [--port PORT]"; exit 1 ;;
+ esac
+done
+
+echo "Checking furt health at $HOST:$PORT..."
+
+# Check if port is listening
+if command -v curl >/dev/null 2>&1; then
+ if curl -s "http://$HOST:$PORT/health" > /tmp/health_response; then
+ echo "Health check successful:"
+ cat /tmp/health_response | sed 's/^/ /'
+ rm -f /tmp/health_response
+ else
+ echo "Health check failed - service not responding"
+ exit 1
+ fi
+else
+ echo "Warning: curl not available, using basic port check"
+ if nc -z "$HOST" "$PORT" 2>/dev/null; then
+ echo "Port $PORT is listening on $HOST"
+ else
+ echo "Port $PORT is not accessible on $HOST"
+ exit 1
+ fi
+fi
+
diff --git a/scripts/manual_mail_test.sh b/scripts/manual_mail_test.sh
new file mode 100755
index 0000000..6a1497c
--- /dev/null
+++ b/scripts/manual_mail_test.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Manual SMTP test with corrected JSON
+
+echo "Testing SMTP with corrected JSON..."
+
+# Simple test without timestamp embedding
+curl -X POST http://127.0.0.1:7811/v1/mail/send \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Furt Test User",
+ "email": "admin@example.com",
+ "subject": "Furt SMTP Test Success!",
+ "message": "This is a test email from Furt Lua HTTP-Server. SMTP Integration working!"
+ }'
+
+echo ""
+echo "Check response above for success:true"
+
diff --git a/scripts/setup-directories.sh b/scripts/setup-directories.sh
new file mode 100755
index 0000000..63881c3
--- /dev/null
+++ b/scripts/setup-directories.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+# scripts/setup-directories.sh - Create directory structure for furt
+
+set -e
+
+# Detect operating system for config directory
+if [ "$(uname)" = "OpenBSD" ]; then
+ CONFIG_DIR="/usr/local/etc/furt"
+ USER="_furt"
+ GROUP="_furt"
+else
+ CONFIG_DIR="/etc/furt"
+ USER="furt"
+ GROUP="furt"
+fi
+
+# Create directories
+mkdir -p "$CONFIG_DIR"
+mkdir -p /usr/local/share/furt
+mkdir -p /var/log/furt
+mkdir -p /var/run/furt
+
+# Set ownership for log directory (service user needs write access)
+chown "$USER:$GROUP" /var/log/furt
+chown "$USER:$GROUP" /var/run/furt
+
+echo "Created directories:"
+echo " Config: $CONFIG_DIR"
+echo " Share: /usr/local/share/furt"
+echo " Logs: /var/log/furt (owned by $USER)"
+echo " PID: /var/run/furt (owned by $USER)"
+
diff --git a/scripts/setup-user.sh b/scripts/setup-user.sh
new file mode 100755
index 0000000..9188626
--- /dev/null
+++ b/scripts/setup-user.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+# scripts/setup-user.sh - Create _furt system user and group
+
+set -e
+
+# Detect operating system
+if [ "$(uname)" = "OpenBSD" ]; then
+ # BSD systems use _furt user convention
+ groupadd _furt 2>/dev/null || true
+ useradd -g _furt -s /bin/false -d /var/empty _furt 2>/dev/null || true
+ echo "Created BSD system user: _furt"
+else
+ # Linux systems use furt user with --system flag
+ groupadd --system furt 2>/dev/null || true
+ useradd --system -g furt -s /bin/false -d /var/empty furt 2>/dev/null || true
+ echo "Created Linux system user: furt"
+fi
+
+echo "User setup completed successfully"
+
diff --git a/scripts/start.sh b/scripts/start.sh
new file mode 100755
index 0000000..1aadf21
--- /dev/null
+++ b/scripts/start.sh
@@ -0,0 +1,126 @@
+#!/bin/sh
+# scripts/start.sh
+set -e
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
+
+echo -e "${GREEN}=== Furt Lua HTTP-Server Startup ===${NC}"
+
+# User can override this manually if needed:
+LUA_COMMAND=""
+
+# Config check first
+if [ "$(uname)" = "OpenBSD" ]; then
+ CONFIG_FILE="/usr/local/etc/furt/furt.conf"
+ PID_FILE="/var/run/furt/furt.pid"
+else
+ CONFIG_FILE="/etc/furt/furt.conf"
+ PID_FILE="/var/run/furt/furt.pid"
+fi
+
+if [ ! -f "$CONFIG_FILE" ] && [ ! -f "$PROJECT_DIR/config/furt.conf" ]; then
+ echo -e "${RED}Error: furt.conf not found${NC}"
+ echo "Create config first in $CONFIG_FILE or $PROJECT_DIR/config/furt.conf"
+ exit 1
+fi
+
+if [ -z "$LUA_COMMAND" ]; then
+ # Test standard distribution paths
+ for cmd in lua51 lua5.1; do
+ if command -v "$cmd" >/dev/null 2>&1; then
+ LUA_COMMAND="$cmd"
+ break
+ fi
+ done
+fi
+
+if [ -z "$LUA_COMMAND" ]; then
+ echo -e "${RED}Error: No Lua 5.1 found${NC}"
+ echo "Install options:"
+ echo " Arch: pacman -S lua51"
+ echo " OpenBSD: pkg_add lua51"
+ echo " Debian: apt install lua5.1"
+ echo ""
+ echo "Or set: LUA_COMMAND=/custom/path/lua51 at top of this script"
+ exit 1
+fi
+
+echo -e "${GREEN}Found Lua:${NC} $LUA_COMMAND"
+
+# Dependency checks
+# Socket check
+$LUA_COMMAND -e "require('socket')" 2>/dev/null || {
+ echo -e "${RED}Error: lua-socket not found${NC}"
+ echo "Install options:"
+ echo " Arch: pacman -S lua51-socket"
+ echo " OpenBSD: pkg_add lua-socket"
+ echo " Debian: apt install lua-socket"
+ exit 1
+}
+
+# JSON library check
+if ! ($LUA_COMMAND -e "require('cjson')" 2>/dev/null || $LUA_COMMAND -e "require('dkjson')" 2>/dev/null); then
+ echo -e "${RED}Error: No JSON library found${NC}"
+ echo "Install options:"
+ echo " Arch: pacman -S lua51-dkjson"
+ echo " OpenBSD: pkg_add lua-cjson"
+ echo " Debian: apt install lua-cjson"
+ exit 1
+fi
+
+# SSL/TLS library check
+$LUA_COMMAND -e "require('ssl')" 2>/dev/null || {
+ echo -e "${RED}Error: SSL/TLS library not found${NC}"
+ echo "Install options:"
+ echo " Arch: pacman -S lua51-sec"
+ echo " OpenBSD: pkg_add luasec"
+ echo " Debian: apt install lua-sec"
+ exit 1
+}
+
+cd "$PROJECT_DIR"
+
+echo -e "${GREEN}Starting Furt...${NC}"
+
+# PID-File cleanup function
+cleanup_pid() {
+ if [ -f "$PID_FILE" ]; then
+ rm -f "$PID_FILE"
+ fi
+}
+
+# Service vs Interactive Detection
+if [ ! -t 0 ] || [ ! -t 1 ]; then
+ # Service mode - Background + PID-File
+ echo -e "${GREEN}Service mode: Background + PID-File${NC}"
+
+ # Start process in background
+ "$LUA_COMMAND" src/main.lua &
+ PID=$!
+
+ # Write PID-File
+ echo "$PID" > "$PID_FILE"
+ echo -e "${GREEN}Furt started (PID: $PID, PID-File: $PID_FILE)${NC}"
+
+ # Verify process is still running after short delay
+ sleep 1
+ if ! kill -0 "$PID" 2>/dev/null; then
+ echo -e "${RED}Error: Process died immediately${NC}"
+ cleanup_pid
+ exit 1
+ fi
+
+ echo -e "${GREEN}Service startup successful${NC}"
+else
+ # Interactive mode - Foreground (no PID-File)
+ echo -e "${GREEN}Interactive mode: Foreground${NC}"
+ exec "$LUA_COMMAND" src/main.lua
+fi
+
diff --git a/scripts/stress_test.sh b/scripts/stress_test.sh
new file mode 100755
index 0000000..05c47ff
--- /dev/null
+++ b/scripts/stress_test.sh
@@ -0,0 +1,171 @@
+#!/bin/bash
+# furt-lua/scripts/stress_test.sh
+# Rate-Limiting und Performance Stress-Test
+
+BASE_URL="http://127.0.0.1:8080"
+# Use correct API keys that match current .env
+API_KEY="YOUR_API_KEY_HERE"
+
+echo "⚡ Furt API Stress Test"
+echo "======================"
+
+# Test 1: Rate-Limiting Test (schnelle Requests)
+echo -e "\n1️⃣ Rate-Limiting Test (20 quick requests):"
+echo "Expected: First ~10 should work, then rate limiting kicks in"
+
+rate_limit_failures=0
+rate_limit_success=0
+
+for i in {1..20}; do
+ response=$(curl -s -w "%{http_code}" \
+ -H "X-API-Key: $API_KEY" \
+ "$BASE_URL/v1/auth/status")
+
+ status=$(echo "$response" | tail -c 4)
+
+ if [ "$status" == "200" ]; then
+ rate_limit_remaining=$(echo "$response" | head -n -1 | jq -r '.rate_limit_remaining // "N/A"' 2>/dev/null)
+ echo "Request $i: ✅ 200 OK (Rate limit remaining: $rate_limit_remaining)"
+ ((rate_limit_success++))
+ elif [ "$status" == "429" ]; then
+ echo "Request $i: ⛔ 429 Rate Limited"
+ ((rate_limit_failures++))
+ else
+ echo "Request $i: ❌ $status Error"
+ fi
+
+ # Small delay to prevent overwhelming
+ sleep 0.1
+done
+
+echo "Rate-Limiting Results: $rate_limit_success success, $rate_limit_failures rate-limited"
+
+# Test 2: Performance Test (concurrent requests)
+echo -e "\n2️⃣ Performance Test (10 concurrent requests):"
+echo "Testing server under concurrent load..."
+
+start_time=$(date +%s.%N)
+
+# Create temp files for results
+temp_dir=$(mktemp -d)
+trap "rm -rf $temp_dir" EXIT
+
+# Launch concurrent requests
+for i in {1..10}; do
+ {
+ local_start=$(date +%s.%N)
+ response=$(curl -s -w "%{http_code}" \
+ -H "X-API-Key: $API_KEY" \
+ "$BASE_URL/health")
+ local_end=$(date +%s.%N)
+
+ status=$(echo "$response" | tail -c 4)
+ duration=$(echo "$local_end - $local_start" | bc -l)
+
+ echo "Concurrent $i: Status $status, Duration ${duration}s" > "$temp_dir/result_$i"
+ } &
+done
+
+# Wait for all background jobs
+wait
+
+end_time=$(date +%s.%N)
+total_duration=$(echo "$end_time - $start_time" | bc -l)
+
+echo "Concurrent Results:"
+cat "$temp_dir"/result_* | sort
+echo "Total Duration: ${total_duration}s"
+
+# Test 3: Mail API Performance (lighter test)
+echo -e "\n3️⃣ Mail API Performance Test (5 requests):"
+echo "Testing mail endpoint performance..."
+
+mail_success=0
+mail_errors=0
+
+for i in {1..5}; do
+ start_time=$(date +%s.%N)
+
+ response=$(curl -s -w "%{http_code}" \
+ -H "X-API-Key: $API_KEY" \
+ -H "Content-Type: application/json" \
+ -d "{\"name\":\"Stress Test $i\",\"email\":\"test$i@example.com\",\"subject\":\"Performance Test\",\"message\":\"Load test message $i\"}" \
+ "$BASE_URL/v1/mail/send")
+
+ end_time=$(date +%s.%N)
+ duration=$(echo "$end_time - $start_time" | bc -l)
+
+ status=$(echo "$response" | tail -c 4)
+
+ if [ "$status" == "200" ]; then
+ echo "Mail $i: ✅ 200 OK (${duration}s)"
+ ((mail_success++))
+ else
+ echo "Mail $i: ❌ Status $status (${duration}s)"
+ ((mail_errors++))
+ fi
+
+ # Delay between mail sends to be nice to SMTP server
+ sleep 1
+done
+
+echo "Mail Performance: $mail_success success, $mail_errors errors"
+
+# Test 4: Mixed Load Test
+echo -e "\n4️⃣ Mixed Load Test (Auth + Health requests):"
+echo "Testing mixed endpoint load..."
+
+mixed_total=0
+mixed_success=0
+
+for i in {1..15}; do
+ ((mixed_total++))
+
+ if [ $((i % 3)) -eq 0 ]; then
+ # Every 3rd request: auth status
+ endpoint="/v1/auth/status"
+ else
+ # Other requests: health check
+ endpoint="/health"
+ fi
+
+ response=$(curl -s -w "%{http_code}" \
+ -H "X-API-Key: $API_KEY" \
+ "$BASE_URL$endpoint")
+
+ status=$(echo "$response" | tail -c 4)
+
+ if [ "$status" == "200" ]; then
+ echo "Mixed $i ($endpoint): ✅ 200 OK"
+ ((mixed_success++))
+ else
+ echo "Mixed $i ($endpoint): ❌ $status"
+ fi
+
+ sleep 0.2
+done
+
+echo "Mixed Load Results: $mixed_success/$mixed_total successful"
+
+# Summary
+echo -e "\n📊 Stress Test Summary:"
+echo "================================="
+echo "Rate-Limiting: $rate_limit_success success, $rate_limit_failures limited (Expected behavior ✅)"
+echo "Concurrent Load: Check above results"
+echo "Mail Performance: $mail_success/$((mail_success + mail_errors)) successful"
+echo "Mixed Load: $mixed_success/$mixed_total successful"
+
+if [ $rate_limit_failures -gt 0 ]; then
+ echo "✅ Rate limiting is working correctly!"
+else
+ echo "⚠️ Rate limiting may need adjustment (no limits hit)"
+fi
+
+if [ $mixed_success -eq $mixed_total ] && [ $mail_success -gt 3 ]; then
+ echo "✅ Server performance looks good!"
+else
+ echo "⚠️ Some performance issues detected"
+fi
+
+echo -e "\n🎯 Next: Check server logs for any errors or memory issues"
+
diff --git a/scripts/sync-files.sh b/scripts/sync-files.sh
new file mode 100755
index 0000000..34d3957
--- /dev/null
+++ b/scripts/sync-files.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+# scripts/sync-files.sh - Copy furt source files to installation directory
+
+set -e
+
+# Check if we're in a furt source directory
+if [ ! -f "src/main.lua" ]; then
+ echo "Error: Not in furt source directory (src/main.lua not found)"
+ exit 1
+fi
+
+# Target directory
+TARGET="/usr/local/share/furt"
+
+echo "Copying furt files to $TARGET..."
+
+# Copy main directories
+cp -r src/ "$TARGET/"
+cp -r config/ "$TARGET/"
+cp -r scripts/ "$TARGET/"
+cp -r integrations/ "$TARGET/"
+
+# Copy version files for merkwerk integration
+[ -f "VERSION" ] && cp VERSION "$TARGET/"
+[ -f ".version_history" ] && cp .version_history "$TARGET/"
+
+# Set proper permissions based on operating system
+if [ "$(uname)" = "OpenBSD" ]; then
+ chown -R root:wheel "$TARGET"
+else
+ chown -R root:root "$TARGET"
+fi
+
+chmod -R 644 "$TARGET"
+find "$TARGET" -type d -exec chmod 755 {} \;
+chmod +x "$TARGET/scripts/start.sh"
+
+echo "Files synced successfully to $TARGET"
+
diff --git a/scripts/test_auth.sh b/scripts/test_auth.sh
new file mode 100755
index 0000000..007179c
--- /dev/null
+++ b/scripts/test_auth.sh
@@ -0,0 +1,79 @@
+#!/bin/bash
+# furt-lua/scripts/test_auth.sh
+# Test API-Key-Authentifizierung (ohne jq parse errors)
+
+BASE_URL="http://127.0.0.1:8080"
+HUGO_API_KEY="YOUR_API_KEY_HERE"
+ADMIN_API_KEY="YOUR_ADMIN_KEY_HERE"
+INVALID_API_KEY="invalid-key-should-fail"
+
+echo "🔐 Testing Furt API-Key Authentication"
+echo "======================================"
+
+# Helper function to make clean API calls
+make_request() {
+ local method="$1"
+ local url="$2"
+ local headers="$3"
+ local data="$4"
+
+ echo "Request: $method $url"
+ if [ -n "$headers" ]; then
+ echo "Headers: $headers"
+ fi
+
+ local response=$(curl -s $method \
+ ${headers:+-H "$headers"} \
+ ${data:+-d "$data"} \
+ -H "Content-Type: application/json" \
+ "$url")
+
+ local status=$(curl -s -o /dev/null -w "%{http_code}" $method \
+ ${headers:+-H "$headers"} \
+ ${data:+-d "$data"} \
+ -H "Content-Type: application/json" \
+ "$url")
+
+ echo "Status: $status"
+ echo "Response: $response" | jq '.' 2>/dev/null || echo "$response"
+ echo ""
+}
+
+# Test 1: Health-Check (public, no auth needed)
+echo "1️⃣ Public Health Check (no auth required):"
+make_request "-X GET" "$BASE_URL/health"
+
+# Test 2: No API-Key -> 401
+echo "2️⃣ Mail without API-Key (should fail with 401):"
+make_request "-X POST" "$BASE_URL/v1/mail/send" "" '{"name":"Test","email":"test@example.com","message":"Test"}'
+
+# Test 3: Invalid API-Key -> 401
+echo "3️⃣ Mail with invalid API-Key (should fail with 401):"
+make_request "-X POST" "$BASE_URL/v1/mail/send" "X-API-Key: $INVALID_API_KEY" '{"name":"Test","email":"test@example.com","message":"Test"}'
+
+# Test 4: Valid API-Key -> 200 (or SMTP error)
+echo "4️⃣ Mail with valid Hugo API-Key (should work):"
+make_request "-X POST" "$BASE_URL/v1/mail/send" "X-API-Key: $HUGO_API_KEY" '{
+ "name": "Test User",
+ "email": "test@example.com",
+ "subject": "API Auth Test",
+ "message": "This is a test message via authenticated API"
+}'
+
+# Test 5: Auth Status Check
+echo "5️⃣ Auth Status Check with Hugo API-Key:"
+make_request "-X GET" "$BASE_URL/v1/auth/status" "X-API-Key: $HUGO_API_KEY"
+
+# Test 6: Auth Status with Admin API-Key
+echo "6️⃣ Auth Status Check with Admin API-Key:"
+make_request "-X GET" "$BASE_URL/v1/auth/status" "X-API-Key: $ADMIN_API_KEY"
+
+echo "✅ Auth Testing Complete!"
+echo ""
+echo "Expected Results:"
+echo "- Test 1: ✅ 200 OK (health check)"
+echo "- Test 2: ❌ 401 Unauthorized (Missing API-Key)"
+echo "- Test 3: ❌ 401 Unauthorized (Invalid API-Key)"
+echo "- Test 4: ✅ 200 OK (Valid API-Key) or 500 if SMTP not configured"
+echo "- Test 5,6: ✅ 200 OK with auth details"
+
diff --git a/scripts/test_curl.sh b/scripts/test_curl.sh
new file mode 100755
index 0000000..39851d8
--- /dev/null
+++ b/scripts/test_curl.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+# furt-lua/scripts/test_curl.sh
+# Manual curl tests for Furt Lua HTTP-Server
+
+set -e
+
+# Colors
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+# Server configuration
+SERVER_URL="http://127.0.0.1:8080"
+
+echo -e "${GREEN}=== Furt HTTP-Server Manual Tests ===${NC}"
+echo -e "${YELLOW}Server:${NC} $SERVER_URL"
+echo ""
+
+# Test 1: Health Check
+echo -e "${YELLOW}Test 1: Health Check${NC}"
+echo "curl -X GET $SERVER_URL/health"
+echo ""
+curl -X GET "$SERVER_URL/health" | jq . 2>/dev/null || curl -X GET "$SERVER_URL/health"
+echo ""
+echo ""
+
+# Test 2: Basic POST Test
+echo -e "${YELLOW}Test 2: Basic POST Test${NC}"
+echo "curl -X POST $SERVER_URL/test -H 'Content-Type: application/json' -d '{\"test\":\"data\"}'"
+echo ""
+curl -X POST "$SERVER_URL/test" \
+ -H "Content-Type: application/json" \
+ -d '{"test":"data","number":42}' | jq . 2>/dev/null || \
+curl -X POST "$SERVER_URL/test" \
+ -H "Content-Type: application/json" \
+ -d '{"test":"data","number":42}'
+echo ""
+echo ""
+
+# Test 3: Mail Endpoint - Valid Data
+echo -e "${YELLOW}Test 3: Mail Endpoint - Valid Data${NC}"
+echo "curl -X POST $SERVER_URL/v1/mail/send -H 'Content-Type: application/json' -d '{...}'"
+echo ""
+curl -X POST "$SERVER_URL/v1/mail/send" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Test User",
+ "email": "test@example.com",
+ "message": "This is a test message from curl"
+ }' | jq . 2>/dev/null || \
+curl -X POST "$SERVER_URL/v1/mail/send" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Test User",
+ "email": "test@example.com",
+ "message": "This is a test message from curl"
+ }'
+echo ""
+echo ""
+
+# Test 4: Mail Endpoint - Invalid Data
+echo -e "${YELLOW}Test 4: Mail Endpoint - Invalid Data (Missing Fields)${NC}"
+echo "curl -X POST $SERVER_URL/v1/mail/send -H 'Content-Type: application/json' -d '{\"name\":\"Test\"}'"
+echo ""
+curl -X POST "$SERVER_URL/v1/mail/send" \
+ -H "Content-Type: application/json" \
+ -d '{"name":"Test"}' | jq . 2>/dev/null || \
+curl -X POST "$SERVER_URL/v1/mail/send" \
+ -H "Content-Type: application/json" \
+ -d '{"name":"Test"}'
+echo ""
+echo ""
+
+# Test 5: 404 Error
+echo -e "${YELLOW}Test 5: 404 Error Handling${NC}"
+echo "curl -X GET $SERVER_URL/nonexistent"
+echo ""
+curl -X GET "$SERVER_URL/nonexistent" | jq . 2>/dev/null || curl -X GET "$SERVER_URL/nonexistent"
+echo ""
+echo ""
+
+# Test 6: Method Not Allowed (if we want to test this)
+echo -e "${YELLOW}Test 6: Wrong Method${NC}"
+echo "curl -X PUT $SERVER_URL/v1/mail/send"
+echo ""
+curl -X PUT "$SERVER_URL/v1/mail/send" | jq . 2>/dev/null || curl -X PUT "$SERVER_URL/v1/mail/send"
+echo ""
+echo ""
+
+echo -e "${GREEN}=== Manual Tests Complete ===${NC}"
+echo -e "${YELLOW}Note:${NC} These tests show the raw HTTP responses."
+echo -e "${YELLOW} For automated testing, use: lua tests/test_http.lua${NC}"
+
diff --git a/scripts/test_modular.sh b/scripts/test_modular.sh
new file mode 100755
index 0000000..85149fe
--- /dev/null
+++ b/scripts/test_modular.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# furt-lua/scripts/test_modular.sh
+# Test der modularen Furt-Architektur
+
+BASE_URL="http://127.0.0.1:8080"
+HUGO_API_KEY="YOUR_API_KEY_HERE"
+
+echo "🧩 Testing Modular Furt Architecture"
+echo "===================================="
+
+# Test 1: Module dependencies check
+echo -e "\n1️⃣ Testing module imports (should not error on startup):"
+echo "Starting server in background..."
+cd "$(dirname "$0")/.."
+lua src/main.lua &
+SERVER_PID=$!
+sleep 2
+
+if kill -0 $SERVER_PID 2>/dev/null; then
+ echo "✅ Server started successfully - all modules loaded"
+else
+ echo "❌ Server failed to start - module import error"
+ exit 1
+fi
+
+# Test 2: Public endpoints (no auth)
+echo -e "\n2️⃣ Testing public endpoints:"
+curl -s -w "Status: %{http_code}\n" "$BASE_URL/health" | jq '.features'
+
+# Test 3: Protected endpoints without auth (should fail)
+echo -e "\n3️⃣ Testing auth protection:"
+curl -s -w "Status: %{http_code}\n" \
+ -X POST \
+ -H "Content-Type: application/json" \
+ -d '{"name":"Test","email":"test@example.com","message":"Test"}' \
+ "$BASE_URL/v1/mail/send" | jq '.error'
+
+# Test 4: Protected endpoints with auth (should work)
+echo -e "\n4️⃣ Testing authenticated request:"
+curl -s -w "Status: %{http_code}\n" \
+ -H "X-API-Key: $HUGO_API_KEY" \
+ "$BASE_URL/v1/auth/status" | jq '.'
+
+# Test 5: Rate limiting headers
+echo -e "\n5️⃣ Testing rate limit headers:"
+curl -s -i -H "X-API-Key: $HUGO_API_KEY" "$BASE_URL/v1/auth/status" | grep -E "X-RateLimit|HTTP"
+
+# Cleanup
+echo -e "\n🧹 Cleanup:"
+kill $SERVER_PID 2>/dev/null
+wait $SERVER_PID 2>/dev/null
+echo "Server stopped"
+
+echo -e "\n✅ Modular Architecture Test Complete!"
+echo "Expected behavior:"
+echo "- Test 1: ✅ Server starts without module errors"
+echo "- Test 2: ✅ Health endpoint works, shows features"
+echo "- Test 3: ❌ 401 Unauthorized (missing API key)"
+echo "- Test 4: ✅ 200 OK with auth details"
+echo "- Test 5: ✅ Rate limit headers present"
+
diff --git a/scripts/test_smtp.sh b/scripts/test_smtp.sh
new file mode 100755
index 0000000..205c0f1
--- /dev/null
+++ b/scripts/test_smtp.sh
@@ -0,0 +1,132 @@
+#!/bin/bash
+# furt-lua/scripts/test_smtp.sh
+# Test SMTP mail functionality
+
+SERVER_URL="http://127.0.0.1:8080"
+
+echo "Testing Furt SMTP Mail Functionality"
+echo "========================================"
+
+# Test 1: Server Health Check
+echo ""
+echo "[1] Testing Health Check..."
+health_response=$(curl -s "$SERVER_URL/health")
+echo "Response: $health_response"
+
+# Check if server is responding
+if echo "$health_response" | grep -q "healthy"; then
+ echo "[OK] Server is healthy"
+else
+ echo "[ERROR] Server not responding or unhealthy"
+ exit 1
+fi
+
+# Test 2: Invalid Mail Request (missing fields)
+echo ""
+echo "[2] Testing validation (missing fields)..."
+invalid_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \
+ -H "Content-Type: application/json" \
+ -d '{"name":"Test"}')
+echo "Response: $invalid_response"
+
+# Check for validation error
+if echo "$invalid_response" | grep -q "Missing required fields"; then
+ echo "[OK] Validation working correctly"
+else
+ echo "[ERROR] Validation failed"
+fi
+
+# Test 3: Invalid Email Format
+echo ""
+echo "[3] Testing email validation..."
+email_validation_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \
+ -H "Content-Type: application/json" \
+ -d '{"name":"Test","email":"invalid-email","message":"Test"}')
+echo "Response: $email_validation_response"
+
+# Check for email validation error
+if echo "$email_validation_response" | grep -q "error"; then
+ echo "[OK] Email validation working"
+else
+ echo "[ERROR] Email validation failed"
+fi
+
+# Test 4: Valid Mail Request (REAL SMTP TEST)
+echo ""
+echo "[4] Testing REAL mail sending..."
+echo "WARNING: This will send a real email to admin@example.com"
+read -p "Continue with real mail test? (y/N): " -n 1 -r
+echo
+
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "Sending real test email..."
+
+ mail_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Furt Test User",
+ "email": "test@example.com",
+ "subject": "Furt SMTP Test - Week 2 Success!",
+ "message": "This is a test email from the Furt Lua HTTP-Server.\n\nSMTP Integration is working!\n\nTimestamp: '$(date)'\nServer: furt-lua v1.0"
+ }')
+
+ echo "Response: $mail_response"
+
+ # Check for success
+ if echo "$mail_response" | grep -q '"success":true'; then
+ echo "[OK] MAIL SENT SUCCESSFULLY!"
+ echo "Check admin@example.com inbox"
+
+ # Extract request ID
+ request_id=$(echo "$mail_response" | grep -o '"request_id":"[^"]*"' | cut -d'"' -f4)
+ echo "Request ID: $request_id"
+ else
+ echo "[ERROR] Mail sending failed"
+ echo "Check server logs and SMTP credentials"
+
+ # Show error details
+ if echo "$mail_response" | grep -q "error"; then
+ error_msg=$(echo "$mail_response" | grep -o '"error":"[^"]*"' | cut -d'"' -f4)
+ echo "Error: $error_msg"
+ fi
+ fi
+else
+ echo "Skipping real mail test"
+fi
+
+# Test 5: Performance Test
+echo ""
+echo "[5] Testing response time..."
+start_time=$(date +%s%N)
+perf_response=$(curl -s "$SERVER_URL/health")
+end_time=$(date +%s%N)
+
+duration_ms=$(( (end_time - start_time) / 1000000 ))
+echo "Response time: ${duration_ms}ms"
+
+if [ $duration_ms -lt 100 ]; then
+ echo "[OK] Response time excellent (< 100ms)"
+elif [ $duration_ms -lt 500 ]; then
+ echo "[OK] Response time good (< 500ms)"
+else
+ echo "[WARN] Response time slow (> 500ms)"
+fi
+
+echo ""
+echo "SMTP Test Complete!"
+echo "===================="
+echo "[OK] Health check working"
+echo "[OK] Input validation working"
+echo "[OK] Email format validation working"
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "Real mail test executed"
+fi
+echo "Performance: ${duration_ms}ms"
+
+echo ""
+echo "Week 2 Challenge Status:"
+echo " SMTP Integration: COMPLETE"
+echo " Environment Variables: CHECK .env"
+echo " Native Lua Implementation: DONE"
+echo " Production Ready: READY FOR TESTING"
+
diff --git a/scripts/validate-config.sh b/scripts/validate-config.sh
new file mode 100755
index 0000000..220cf69
--- /dev/null
+++ b/scripts/validate-config.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+# scripts/validate-config.sh - Validate furt configuration
+
+set -e
+
+# Detect config file location
+if [ "$(uname)" = "OpenBSD" ]; then
+ CONFIG_FILE="/usr/local/etc/furt/furt.conf"
+else
+ CONFIG_FILE="/etc/furt/furt.conf"
+fi
+
+echo "Validating configuration: $CONFIG_FILE"
+
+# Check if config file exists
+if [ ! -f "$CONFIG_FILE" ]; then
+ echo "Error: Configuration file not found: $CONFIG_FILE"
+ exit 1
+fi
+
+# Basic INI syntax validation
+if ! grep -q '^\[server\]' "$CONFIG_FILE"; then
+ echo "Error: [server] section missing in config"
+ exit 1
+fi
+
+# Fix: Use POSIX-compatible regex patterns
+if ! grep -q '^[ \t]*port[ \t]*=' "$CONFIG_FILE"; then
+ echo "Error: server port not configured"
+ exit 1
+fi
+
+if ! grep -q '^[ \t]*host[ \t]*=' "$CONFIG_FILE"; then
+ echo "Error: server host not configured"
+ exit 1
+fi
+
+# Check for at least one API key
+if ! grep -q '^\[api_key' "$CONFIG_FILE"; then
+ echo "Warning: No API keys configured"
+fi
+
+# Check permissions (should not be world-readable due to secrets)
+PERMS=$(stat -c '%a' "$CONFIG_FILE" 2>/dev/null || stat -f '%Lp' "$CONFIG_FILE")
+if [ "$PERMS" -gt 640 ]; then
+ echo "Warning: Config file permissions too open ($PERMS), should be 640"
+fi
+
+echo "Configuration validation completed"
+
diff --git a/src/auth.lua b/src/auth.lua
new file mode 100644
index 0000000..93340bb
--- /dev/null
+++ b/src/auth.lua
@@ -0,0 +1,139 @@
+-- furt-lua/src/auth.lua
+-- API Key authentication system
+-- Dragons@Work Digital Sovereignty Project
+
+local IpUtils = require("src.ip_utils")
+local RateLimiter = require("src.rate_limiter")
+
+local Auth = {}
+
+-- Load configuration
+local config = require("config.server")
+
+-- Authenticate incoming request
+function Auth.authenticate_request(request)
+ local api_key = request.headers["x-api-key"]
+
+ if not api_key then
+ return false, "Missing X-API-Key header", 401
+ end
+
+ -- Check if API key exists in config
+ local key_config = config.api_keys and config.api_keys[api_key]
+ if not key_config then
+ return false, "Invalid API key", 401
+ end
+
+ -- Get client IP
+ local client_ip = IpUtils.get_client_ip(request)
+
+ -- Check IP restrictions
+ if not IpUtils.is_ip_allowed(client_ip, key_config.allowed_ips) then
+ return false, "IP address not allowed", 403
+ end
+
+ -- Check rate limits
+ local rate_ok, rate_message, rate_info = RateLimiter:check_api_and_ip_limits(api_key, client_ip)
+ if not rate_ok then
+ return false, rate_message, 429, rate_info
+ end
+
+ -- Return auth context
+ return true, {
+ api_key = api_key,
+ key_name = key_config.name,
+ permissions = key_config.permissions or {},
+ client_ip = client_ip,
+ rate_info = rate_info
+ }
+end
+
+-- Check if user has specific permission
+function Auth.has_permission(auth_context, required_permission)
+ if not auth_context or not auth_context.permissions then
+ return false
+ end
+
+ -- No permission required = always allow for authenticated users
+ if not required_permission then
+ return true
+ end
+
+ -- Check for specific permission or wildcard
+ for _, permission in ipairs(auth_context.permissions) do
+ if permission == required_permission or permission == "*" then
+ return true
+ end
+ end
+
+ return false
+end
+
+-- Create auth middleware wrapper for route handlers
+function Auth.create_protected_route(required_permission, handler)
+ return function(request, server)
+ -- Authenticate request
+ local auth_success, auth_result, status_code, rate_info = Auth.authenticate_request(request)
+
+ if not auth_success then
+ local error_response = {
+ error = auth_result,
+ timestamp = os.time()
+ }
+
+ -- Add rate limit info to error if available
+ if rate_info then
+ error_response.rate_limit = rate_info
+ end
+
+ return server:create_response(status_code or 401, error_response, nil, nil, request)
+ end
+
+ -- Check permissions
+ if required_permission and not Auth.has_permission(auth_result, required_permission) then
+ return server:create_response(403, {
+ error = "Insufficient permissions",
+ required = required_permission,
+ available = auth_result.permissions
+ }, nil, nil, request)
+ end
+
+ -- Add auth context to request
+ request.auth = auth_result
+
+ -- Get rate limit headers
+ local rate_headers = RateLimiter:get_rate_limit_headers(auth_result.rate_info)
+
+ -- Call original handler
+ local result = handler(request, server)
+
+ -- If result is a string (already formatted response), return as-is
+ if type(result) == "string" then
+ return result
+ end
+
+ -- If handler returned data, create response with rate limit headers
+ return server:create_response(200, result, "application/json", rate_headers, request)
+ end
+end
+
+-- Get authentication status for debug/monitoring
+function Auth.get_auth_status(auth_context)
+ if not auth_context then
+ return {
+ authenticated = false
+ }
+ end
+
+ return {
+ authenticated = true,
+ api_key_name = auth_context.key_name,
+ permissions = auth_context.permissions,
+ client_ip = auth_context.client_ip,
+ rate_limit_remaining = auth_context.rate_info and auth_context.rate_info.api_key and auth_context.rate_info.api_key.remaining,
+ ip_rate_limit_remaining = auth_context.rate_info and auth_context.rate_info.ip and auth_context.rate_info.ip.remaining
+ }
+end
+
+return Auth
+
diff --git a/src/config_parser.lua b/src/config_parser.lua
new file mode 100644
index 0000000..8760014
--- /dev/null
+++ b/src/config_parser.lua
@@ -0,0 +1,237 @@
+-- src/config_parser.lua
+-- nginx-style configuration parser for Multi-Tenant setup
+-- Dragons@Work Digital Sovereignty Project
+-- Lua 5.1 compatible (no goto statements)
+
+local ConfigParser = {}
+
+-- Parse nginx-style config file
+function ConfigParser.parse_file(config_path)
+ local file = io.open(config_path, "r")
+ if not file then
+ error("Could not open config file: " .. config_path)
+ end
+
+ local config = {
+ server = {},
+ api_keys = {},
+ smtp_default = {}
+ }
+
+ local current_section = nil
+ local current_api_key = nil
+ local line_number = 0
+
+ for line in file:lines() do
+ line_number = line_number + 1
+
+ -- Skip empty lines and comments
+ line = line:match("^%s*(.-)%s*$") -- trim whitespace
+ if not (line == "" or line:match("^#")) then
+ -- Section headers: [section] or [api_key "keyname"]
+ local section_match = line:match("^%[([^%]]+)%]$")
+ if section_match then
+ if section_match:match("^api_key") then
+ -- Extract API key from [api_key "keyname"]
+ local key_name = section_match:match('^api_key%s+"([^"]+)"$')
+ if not key_name then
+ error(string.format("Invalid api_key section at line %d: %s", line_number, line))
+ end
+ current_api_key = key_name
+ current_section = "api_key"
+ config.api_keys[key_name] = {}
+ else
+ current_section = section_match
+ current_api_key = nil
+ if not config[current_section] then
+ config[current_section] = {}
+ end
+ end
+ else
+ -- Key-value pairs: key = value
+ local key, value = line:match("^([^=]+)=(.+)$")
+ if key and value then
+ key = key:match("^%s*(.-)%s*$") -- trim
+ value = value:match("^%s*(.-)%s*$") -- trim
+
+ -- Remove quotes from value if present
+ value = value:match('^"(.*)"$') or value
+
+ if current_section == "api_key" and current_api_key then
+ ConfigParser.set_api_key_value(config.api_keys[current_api_key], key, value)
+ elseif current_section then
+ ConfigParser.set_config_value(config[current_section], key, value)
+ else
+ error(string.format("Key-value pair outside section at line %d: %s", line_number, line))
+ end
+ else
+ error(string.format("Invalid line format at line %d: %s", line_number, line))
+ end
+ end
+ end
+ end
+
+ file:close()
+
+ -- Validate required sections
+ ConfigParser.validate_config(config)
+
+ return config
+end
+
+-- Set configuration value with type conversion
+function ConfigParser.set_config_value(section, key, value)
+ -- Convert numeric values
+ local num_value = tonumber(value)
+ if num_value then
+ section[key] = num_value
+ return
+ end
+
+ -- Convert boolean values
+ if value:lower() == "true" then
+ section[key] = true
+ return
+ elseif value:lower() == "false" then
+ section[key] = false
+ return
+ end
+
+ -- Keep as string
+ section[key] = value
+end
+
+-- Set API key configuration value
+function ConfigParser.set_api_key_value(api_key_config, key, value)
+ -- Handle special multi-value fields
+ if key == "permissions" then
+ api_key_config.permissions = {}
+ for perm in value:gmatch("([^,]+)") do
+ table.insert(api_key_config.permissions, perm:match("^%s*(.-)%s*$"))
+ end
+ return
+ end
+
+ if key == "allowed_ips" then
+ api_key_config.allowed_ips = {}
+ for ip in value:gmatch("([^,]+)") do
+ table.insert(api_key_config.allowed_ips, ip:match("^%s*(.-)%s*$"))
+ end
+ return
+ end
+
+ -- Regular key-value assignment with type conversion
+ ConfigParser.set_config_value(api_key_config, key, value)
+end
+
+-- Validate required configuration
+function ConfigParser.validate_config(config)
+ -- Check required server settings
+ if not config.server.port then
+ error("server.port is required")
+ end
+
+ if not config.server.host then
+ config.server.host = "127.0.0.1" -- default
+ end
+
+ -- Check that we have at least one API key
+ local key_count = 0
+ for _ in pairs(config.api_keys) do
+ key_count = key_count + 1
+ end
+
+ if key_count == 0 then
+ print("Warning: No API keys configured")
+ end
+
+ -- Validate each API key
+ for key_name, key_config in pairs(config.api_keys) do
+ if not key_config.name then
+ error("API key '" .. key_name .. "' missing name")
+ end
+
+ if not key_config.permissions then
+ key_config.permissions = {} -- empty permissions
+ end
+
+ if not key_config.allowed_ips then
+ key_config.allowed_ips = {} -- no IP restrictions
+ end
+
+ -- Validate mail configuration only if API key has mail:send permission
+ local has_mail_permission = false
+ if key_config.permissions then
+ for _, perm in ipairs(key_config.permissions) do
+ if perm == "mail:send" or perm == "*" then
+ has_mail_permission = true
+ break
+ end
+ end
+ end
+
+ if has_mail_permission then
+ if not key_config.mail_to then
+ error("API key '" .. key_name .. "' missing mail_to")
+ end
+
+ if not key_config.mail_from then
+ error("API key '" .. key_name .. "' missing mail_from")
+ end
+ end
+ end
+
+ -- Set SMTP defaults if not configured
+ if not config.smtp_default.host then
+ config.smtp_default.host = "localhost"
+ config.smtp_default.port = 25
+ print("Warning: No default SMTP configured, using localhost:25")
+ end
+end
+
+-- Get mail configuration for specific API key
+function ConfigParser.get_mail_config_for_api_key(config, api_key)
+ local key_config = config.api_keys[api_key]
+ if not key_config then
+ return nil, "API key not found"
+ end
+
+ return {
+ -- Recipient and sender
+ to_address = key_config.mail_to,
+ from_address = key_config.mail_from,
+ subject_prefix = key_config.mail_subject_prefix or "",
+
+ -- SMTP settings: key-specific or default
+ smtp_server = key_config.mail_smtp_host or config.smtp_default.host,
+ smtp_port = key_config.mail_smtp_port or config.smtp_default.port,
+ username = key_config.mail_smtp_user or config.smtp_default.user,
+ password = key_config.mail_smtp_pass or config.smtp_default.password,
+ use_ssl = key_config.mail_smtp_ssl or config.smtp_default.use_ssl or true
+ }
+end
+
+-- Load configuration from file with fallback
+function ConfigParser.load_config()
+ -- Try different locations based on OS
+ local config_paths = {
+ "/usr/local/etc/furt/furt.conf", -- OpenBSD
+ "/etc/furt/furt.conf", -- Linux
+ "config/furt.conf", -- Development
+ "furt.conf" -- Current directory
+ }
+
+ for _, path in ipairs(config_paths) do
+ local file = io.open(path, "r")
+ if file then
+ file:close()
+ print("Loading config from: " .. path)
+ return ConfigParser.parse_file(path)
+ end
+ end
+
+ error("No configuration file found. Tried: " .. table.concat(config_paths, ", "))
+end
+
+return ConfigParser
+
diff --git a/src/http_server.lua b/src/http_server.lua
new file mode 100644
index 0000000..c9b85b2
--- /dev/null
+++ b/src/http_server.lua
@@ -0,0 +1,255 @@
+-- src/http_server.lua
+-- HTTP Server Core for Furt API-Gateway
+-- Dragons@Work Digital Sovereignty Project
+
+local socket = require("socket")
+local found_cjson, cjson = pcall(require, 'cjson')
+if not found_cjson then
+ cjson = require('dkjson')
+end
+
+local config = require("config.server")
+local Auth = require("src.auth")
+
+-- HTTP-Server Module
+local FurtServer = {}
+
+function FurtServer:new()
+ local instance = {
+ server = nil,
+ port = config.port or 7811,
+ host = config.host or "127.0.0.1",
+ routes = {}
+ }
+ setmetatable(instance, self)
+ self.__index = self
+ return instance
+end
+
+-- Add route handler
+function FurtServer:add_route(method, path, handler)
+ if not self.routes[method] then
+ self.routes[method] = {}
+ end
+ self.routes[method][path] = handler
+end
+
+-- Add protected route (requires authentication)
+function FurtServer:add_protected_route(method, path, required_permission, handler)
+ self:add_route(method, path, Auth.create_protected_route(required_permission, handler))
+end
+
+-- Parse HTTP request
+function FurtServer:parse_request(client)
+ local request_line = client:receive()
+ if not request_line then
+ return nil
+ end
+
+ -- Parse request line: "POST /v1/mail/send HTTP/1.1"
+ local method, path, protocol = request_line:match("(%w+) (%S+) (%S+)")
+ if not method then
+ return nil
+ end
+
+ -- Parse headers
+ local headers = {}
+ local content_length = 0
+
+ while true do
+ local line = client:receive()
+ if not line or line == "" then
+ break
+ end
+
+ local key, value = line:match("([^:]+): (.+)")
+ if key and value then
+ headers[key:lower()] = value
+ if key:lower() == "content-length" then
+ content_length = tonumber(value) or 0
+ end
+ end
+ end
+
+ -- Parse body
+ local body = ""
+ if content_length > 0 then
+ body = client:receive(content_length)
+ end
+
+ return {
+ method = method,
+ path = path,
+ protocol = protocol,
+ headers = headers,
+ body = body,
+ content_length = content_length
+ }
+end
+
+-- Add CORS headers with configurable origins
+function FurtServer:add_cors_headers(request)
+ local allowed_origins = config.cors and config.cors.allowed_origins or {
+ "http://localhost:1313",
+ "http://127.0.0.1:1313"
+ }
+
+ -- Check if request has Origin header
+ local origin = request and request.headers and request.headers.origin
+ local cors_origin = "*" -- Default fallback
+
+ -- If origin is provided and in allowed list, use it
+ if origin then
+ for _, allowed in ipairs(allowed_origins) do
+ if origin == allowed then
+ cors_origin = origin
+ break
+ end
+ end
+ end
+
+ return {
+ ["Access-Control-Allow-Origin"] = cors_origin,
+ ["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS",
+ ["Access-Control-Allow-Headers"] = "Content-Type, X-API-Key, Authorization, Accept",
+ ["Access-Control-Max-Age"] = "86400",
+ ["Access-Control-Allow-Credentials"] = "false"
+ }
+end
+
+-- Create HTTP response
+function FurtServer:create_response(status, data, content_type, additional_headers, request)
+ content_type = content_type or "application/json"
+ local body = ""
+
+ if type(data) == "table" then
+ body = cjson.encode(data)
+ else
+ body = tostring(data or "")
+ end
+
+ -- Start with CORS headers
+ local headers = self:add_cors_headers(request)
+
+ -- Add standard headers
+ headers["Content-Type"] = content_type
+ headers["Content-Length"] = tostring(#body)
+ headers["Connection"] = "close"
+ headers["Server"] = "Furt-Lua/1.1"
+
+ -- Add additional headers if provided
+ if additional_headers then
+ for key, value in pairs(additional_headers) do
+ headers[key] = value
+ end
+ end
+
+ -- Build response
+ local response = string.format("HTTP/1.1 %d %s\r\n", status, self:get_status_text(status))
+
+ for key, value in pairs(headers) do
+ response = response .. key .. ": " .. value .. "\r\n"
+ end
+
+ response = response .. "\r\n" .. body
+
+ return response
+end
+
+-- Get HTTP status text
+function FurtServer:get_status_text(status)
+ local status_texts = {
+ [200] = "OK",
+ [204] = "No Content",
+ [400] = "Bad Request",
+ [401] = "Unauthorized",
+ [403] = "Forbidden",
+ [404] = "Not Found",
+ [405] = "Method Not Allowed",
+ [429] = "Too Many Requests",
+ [500] = "Internal Server Error"
+ }
+ return status_texts[status] or "Unknown"
+end
+
+-- Handle client request
+function FurtServer:handle_client(client)
+ local request = self:parse_request(client)
+ if not request then
+ local response = self:create_response(400, {error = "Invalid request"}, nil, nil, nil)
+ client:send(response)
+ return
+ end
+
+ print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"),
+ request.method, request.path))
+
+ -- Handle OPTIONS preflight requests (CORS)
+ if request.method == "OPTIONS" then
+ local response = self:create_response(204, "", "text/plain", nil, request)
+ client:send(response)
+ return
+ end
+
+ -- Route handling
+ local handler = nil
+ if self.routes[request.method] and self.routes[request.method][request.path] then
+ handler = self.routes[request.method][request.path]
+ end
+
+ if handler then
+ local success, result = pcall(handler, request, self)
+ if success then
+ client:send(result)
+ else
+ print("Handler error: " .. tostring(result))
+ local error_response = self:create_response(500, {error = "Internal server error"}, nil, nil, request)
+ client:send(error_response)
+ end
+ else
+ print("Route not found: " .. request.method .. " " .. request.path)
+ local response = self:create_response(404, {error = "Route not found", method = request.method, path = request.path}, nil, nil, request)
+ client:send(response)
+ end
+end
+
+-- Start HTTP server
+function FurtServer:start()
+ self.server = socket.bind(self.host, self.port)
+ if not self.server then
+ error("Failed to bind to " .. self.host .. ":" .. self.port)
+ end
+
+ local HealthRoute = require("src.routes.health")
+ local version_info = HealthRoute.get_version_info()
+
+ print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port))
+ print("Version: " .. version_info.version .. " (merkwerk)")
+ print("Content-Hash: " .. (version_info.content_hash or "unknown"))
+ print("VCS: " .. (version_info.vcs_info and version_info.vcs_info.hash or "none"))
+ print("API-Key authentication: ENABLED")
+
+ -- Show actual configured rate limits
+ local rate_limits = config.security and config.security.rate_limits
+ if rate_limits then
+ print(string.format("Rate limiting: ENABLED (%d req/hour per API key, %d req/hour per IP)",
+ rate_limits.api_key_max, rate_limits.ip_max))
+ else
+ print("Rate limiting: ENABLED (default values)")
+ end
+
+ print("CORS enabled for " .. (#config.cors.allowed_origins) .. " configured origins")
+ print("Press Ctrl+C to stop")
+
+ while true do
+ local client = self.server:accept()
+ if client then
+ client:settimeout(10) -- 10 second timeout
+ self:handle_client(client)
+ client:close()
+ end
+ end
+end
+
+return FurtServer
+
diff --git a/src/ip_utils.lua b/src/ip_utils.lua
new file mode 100644
index 0000000..a4eefd4
--- /dev/null
+++ b/src/ip_utils.lua
@@ -0,0 +1,117 @@
+-- furt-lua/src/ip_utils.lua
+-- IP address and CIDR utilities
+-- Dragons@Work Digital Sovereignty Project
+
+local IpUtils = {}
+
+-- Simple bitwise AND for Lua 5.1 compatibility
+local function bitwise_and(a, b)
+ local result = 0
+ local bit = 1
+ while a > 0 or b > 0 do
+ if (a % 2 == 1) and (b % 2 == 1) then
+ result = result + bit
+ end
+ a = math.floor(a / 2)
+ b = math.floor(b / 2)
+ bit = bit * 2
+ end
+ return result
+end
+
+-- Create subnet mask for given CIDR bits
+local function create_mask(mask_bits)
+ if mask_bits >= 32 then
+ return 0xFFFFFFFF
+ elseif mask_bits <= 0 then
+ return 0
+ else
+ -- Create mask: 32-bit with 'mask_bits' ones from left
+ local mask = 0
+ for i = 0, mask_bits - 1 do
+ mask = mask + math.pow(2, 31 - i)
+ end
+ return mask
+ end
+end
+
+-- CIDR IP matching function (Lua 5.1 compatible)
+function IpUtils.ip_matches_cidr(ip, cidr)
+ if not cidr:find("/") then
+ -- No subnet mask, direct comparison
+ return ip == cidr
+ end
+
+ local network, mask_bits = cidr:match("([^/]+)/(%d+)")
+ if not network or not mask_bits then
+ return false
+ end
+
+ mask_bits = tonumber(mask_bits)
+
+ -- Simple IPv4 CIDR matching
+ if ip:find("%.") and network:find("%.") then
+ -- Convert IPv4 to number
+ local function ip_to_num(ip_str)
+ local parts = {}
+ for part in ip_str:gmatch("(%d+)") do
+ table.insert(parts, tonumber(part))
+ end
+ if #parts == 4 then
+ return (parts[1] * 16777216) + (parts[2] * 65536) + (parts[3] * 256) + parts[4]
+ end
+ return 0
+ end
+
+ local ip_num = ip_to_num(ip)
+ local network_num = ip_to_num(network)
+
+ -- Create subnet mask
+ local mask = create_mask(mask_bits)
+
+ -- Apply mask to both IPs and compare
+ return bitwise_and(ip_num, mask) == bitwise_and(network_num, mask)
+ end
+
+ -- Fallback: if CIDR parsing fails, allow if IP matches network part
+ return ip == network or ip:find("^" .. network:gsub("%.", "%%."))
+end
+
+-- Check if IP is in allowed list
+function IpUtils.is_ip_allowed(client_ip, allowed_ips)
+ if not allowed_ips or #allowed_ips == 0 then
+ return true -- No restrictions
+ end
+
+ for _, allowed_cidr in ipairs(allowed_ips) do
+ if IpUtils.ip_matches_cidr(client_ip, allowed_cidr) then
+ return true
+ end
+ end
+
+ return false
+end
+
+-- Extract client IP (considering proxies)
+function IpUtils.get_client_ip(request)
+ -- Check for forwarded IP headers first
+ local forwarded_for = request.headers["x-forwarded-for"]
+ if forwarded_for then
+ -- Take first IP from comma-separated list
+ local first_ip = forwarded_for:match("([^,]+)")
+ if first_ip then
+ return first_ip:match("^%s*(.-)%s*$") -- trim whitespace
+ end
+ end
+
+ local real_ip = request.headers["x-real-ip"]
+ if real_ip then
+ return real_ip
+ end
+
+ -- Fallback to connection IP (would need socket info, defaulting to localhost for now)
+ return "127.0.0.1"
+end
+
+return IpUtils
+
diff --git a/src/main.lua b/src/main.lua
new file mode 100644
index 0000000..0949efe
--- /dev/null
+++ b/src/main.lua
@@ -0,0 +1,34 @@
+-- src/main.lua
+-- Furt API-Gateway - Application Entry Point
+-- Dragons@Work Digital Sovereignty Project
+
+-- Load HTTP Server Core
+local FurtServer = require("src.http_server")
+
+-- Load Route Modules
+local MailRoute = require("src.routes.mail")
+local AuthRoute = require("src.routes.auth")
+local HealthRoute = require("src.routes.health")
+
+-- Load configuration
+local config = require("config.server")
+
+-- Initialize server
+local server = FurtServer:new()
+
+-- Register public routes (no authentication required)
+server:add_route("GET", "/health", HealthRoute.handle_health)
+
+-- Test endpoint for development (configurable via furt.conf)
+if config.security and config.security.enable_test_endpoint then
+ server:add_route("POST", "/test", HealthRoute.handle_test)
+ print("[WARN] Test endpoint enabled via configuration")
+end
+
+-- Register protected routes (require authentication)
+server:add_protected_route("POST", "/v1/mail/send", "mail:send", MailRoute.handle_mail_send)
+server:add_protected_route("GET", "/v1/auth/status", nil, AuthRoute.handle_auth_status)
+
+-- Start server
+server:start()
+
diff --git a/src/rate_limiter.lua b/src/rate_limiter.lua
new file mode 100644
index 0000000..07b7a49
--- /dev/null
+++ b/src/rate_limiter.lua
@@ -0,0 +1,153 @@
+-- src/rate_limiter.lua
+-- Rate limiting system for API requests
+-- Dragons@Work Digital Sovereignty Project
+
+local RateLimiter = {
+ requests = {}, -- {api_key = {timestamps}, ip = {timestamps}}
+ cleanup_interval = 300, -- Cleanup every 5 minutes
+ last_cleanup = os.time(),
+
+ -- Default limits (configurable)
+ default_limits = {
+ api_key_max = 60, -- 60 requests per hour per API key
+ ip_max = 100, -- 100 requests per hour per IP
+ window = 3600 -- 1 hour window
+ }
+}
+
+-- Configure rate limits from config
+function RateLimiter:configure(limits)
+ if limits then
+ if limits.api_key_max then
+ self.default_limits.api_key_max = limits.api_key_max
+ end
+ if limits.ip_max then
+ self.default_limits.ip_max = limits.ip_max
+ end
+ if limits.window then
+ self.default_limits.window = limits.window
+ end
+
+ print("Rate limiting configured:")
+ print(" API Key limit: " .. self.default_limits.api_key_max .. " req/hour")
+ print(" IP limit: " .. self.default_limits.ip_max .. " req/hour")
+ print(" Window: " .. self.default_limits.window .. " seconds")
+ end
+end
+
+-- Cleanup old requests from memory
+function RateLimiter:cleanup_old_requests()
+ local now = os.time()
+ if now - self.last_cleanup < self.cleanup_interval then
+ return
+ end
+
+ local cutoff = now - self.default_limits.window
+
+ for key, timestamps in pairs(self.requests) do
+ local filtered = {}
+ for _, timestamp in ipairs(timestamps) do
+ if timestamp > cutoff then
+ table.insert(filtered, timestamp)
+ end
+ end
+ self.requests[key] = filtered
+ end
+
+ self.last_cleanup = now
+end
+
+-- Check if request is within rate limit
+function RateLimiter:check_rate_limit(key, max_requests, window_seconds)
+ self:cleanup_old_requests()
+
+ local now = os.time()
+ local cutoff = now - (window_seconds or self.default_limits.window)
+
+ if not self.requests[key] then
+ self.requests[key] = {}
+ end
+
+ -- Count requests in time window
+ local count = 0
+ for _, timestamp in ipairs(self.requests[key]) do
+ if timestamp > cutoff then
+ count = count + 1
+ end
+ end
+
+ -- Check if limit exceeded
+ if count >= max_requests then
+ return false, count, max_requests - count
+ end
+
+ -- Record this request
+ table.insert(self.requests[key], now)
+
+ return true, count + 1, max_requests - (count + 1)
+end
+
+-- Check rate limits for API key and IP
+function RateLimiter:check_api_and_ip_limits(api_key, client_ip)
+ -- Check API key rate limit
+ local api_key_allowed, api_count, api_remaining = self:check_rate_limit(
+ "api_key:" .. api_key,
+ self.default_limits.api_key_max,
+ self.default_limits.window
+ )
+
+ if not api_key_allowed then
+ return false, "API key rate limit exceeded", {
+ type = "api_key",
+ current = api_count,
+ limit = self.default_limits.api_key_max,
+ remaining = api_remaining
+ }
+ end
+
+ -- Check IP rate limit
+ local ip_allowed, ip_count, ip_remaining = self:check_rate_limit(
+ "ip:" .. client_ip,
+ self.default_limits.ip_max,
+ self.default_limits.window
+ )
+
+ if not ip_allowed then
+ return false, "IP rate limit exceeded", {
+ type = "ip",
+ current = ip_count,
+ limit = self.default_limits.ip_max,
+ remaining = ip_remaining
+ }
+ end
+
+ -- Both limits OK
+ return true, "OK", {
+ api_key = {
+ current = api_count,
+ limit = self.default_limits.api_key_max,
+ remaining = api_remaining
+ },
+ ip = {
+ current = ip_count,
+ limit = self.default_limits.ip_max,
+ remaining = ip_remaining
+ }
+ }
+end
+
+-- Get rate limit headers for HTTP response
+function RateLimiter:get_rate_limit_headers(limit_info)
+ if not limit_info or not limit_info.api_key then
+ return {}
+ end
+
+ return {
+ ["X-RateLimit-Remaining"] = tostring(limit_info.api_key.remaining or 0),
+ ["X-RateLimit-Limit"] = tostring(self.default_limits.api_key_max),
+ ["X-RateLimit-Window"] = tostring(self.default_limits.window)
+ }
+end
+
+return RateLimiter
+
diff --git a/src/routes/auth.lua b/src/routes/auth.lua
new file mode 100644
index 0000000..a0fad33
--- /dev/null
+++ b/src/routes/auth.lua
@@ -0,0 +1,16 @@
+-- furt-lua/src/routes/auth.lua
+-- Authentication status route handler
+-- Dragons@Work Digital Sovereignty Project
+
+local Auth = require("src.auth")
+
+local AuthRoute = {}
+
+-- Auth status endpoint handler
+function AuthRoute.handle_auth_status(request, server)
+ -- Return authentication status
+ return Auth.get_auth_status(request.auth)
+end
+
+return AuthRoute
+
diff --git a/src/routes/health.lua b/src/routes/health.lua
new file mode 100644
index 0000000..ac93338
--- /dev/null
+++ b/src/routes/health.lua
@@ -0,0 +1,80 @@
+-- src/routes/health.lua
+-- Health monitoring and diagnostic routes for Furt API-Gateway
+-- Dragons@Work Digital Sovereignty Project
+
+local found_cjson, cjson = pcall(require, 'cjson')
+if not found_cjson then
+ cjson = require('dkjson')
+end
+
+local config = require("config.server")
+
+local HealthRoute = {}
+
+-- Get version information from merkwerk integration
+function HealthRoute.get_version_info()
+ -- Load merkwerk integration
+ local success, merkwerk = pcall(require, "integrations.lua-api")
+ if not success then
+ print("WARNING: merkwerk integration not available, using fallback")
+ return {
+ service = "furt-lua",
+ version = "?.?.?",
+ content_hash = "unknown",
+ vcs_info = { type = "none", hash = "", branch = "" },
+ source = "fallback-no-merkwerk"
+ }
+ end
+
+ -- Get merkwerk health info
+ local health_info = merkwerk.get_health_info()
+
+ -- Ensure compatibility with old VERSION-only expectations
+ if not health_info.version then
+ health_info.version = "?.?.?"
+ end
+
+ return health_info
+end
+
+-- Handle /health endpoint - system health check
+function HealthRoute.handle_health(request, server)
+ local version_info = HealthRoute.get_version_info()
+ local response_data = {
+ status = "healthy",
+ service = version_info.service or "furt-lua",
+ version = version_info.version,
+ content_hash = version_info.content_hash,
+ vcs_info = version_info.vcs_info,
+ timestamp = os.time(),
+ source = version_info.source,
+ features = {
+ smtp_configured = config.smtp_default and config.smtp_default.host ~= nil,
+ auth_enabled = true,
+ rate_limiting = true,
+ rate_limits = config.security and config.security.rate_limits,
+ merkwerk_integrated = version_info.source == "merkwerk"
+ }
+ }
+ return server:create_response(200, response_data, nil, nil, request)
+end
+
+-- Handle /test endpoint - development testing (configurable)
+function HealthRoute.handle_test(request, server)
+ local response_data = {
+ message = "Test endpoint working",
+ received_data = request.body,
+ headers_count = 0,
+ warning = "This is a development endpoint (enabled via config)"
+ }
+
+ -- Count headers
+ for _ in pairs(request.headers) do
+ response_data.headers_count = response_data.headers_count + 1
+ end
+
+ return server:create_response(200, response_data, nil, nil, request)
+end
+
+return HealthRoute
+
diff --git a/src/routes/mail.lua b/src/routes/mail.lua
new file mode 100644
index 0000000..8e59b03
--- /dev/null
+++ b/src/routes/mail.lua
@@ -0,0 +1,169 @@
+-- src/routes/mail.lua
+-- Multi-Tenant Mail service route handler
+-- API-Key determines mail configuration and recipient
+-- Dragons@Work Digital Sovereignty Project
+
+local found_cjson, cjson = pcall(require, 'cjson')
+if not found_cjson then
+ cjson = require('dkjson')
+end
+
+local MailRoute = {}
+
+-- Load configuration
+local config = require("config.server")
+
+-- Validate email format
+local function validate_email(email)
+ return email and email:match("^[^@]+@[^@]+%.[^@]+$") ~= nil
+end
+
+-- Validate required fields
+local function validate_mail_data(data)
+ if not data.name or type(data.name) ~= "string" or data.name:match("^%s*$") then
+ return false, "Name is required and cannot be empty"
+ end
+
+ if not data.email or not validate_email(data.email) then
+ return false, "Valid email address is required"
+ end
+
+ if not data.message or type(data.message) ~= "string" or data.message:match("^%s*$") then
+ return false, "Message is required and cannot be empty"
+ end
+
+ -- Optional subject validation
+ if data.subject and (type(data.subject) ~= "string" or #data.subject > 200) then
+ return false, "Subject must be a string with max 200 characters"
+ end
+
+ -- Message length validation
+ if #data.message > 5000 then
+ return false, "Message too long (max 5000 characters)"
+ end
+
+ return true
+end
+
+-- Generate unique request ID
+local function generate_request_id()
+ return os.time() .. "-" .. math.random(1000, 9999)
+end
+
+-- Get tenant-specific mail configuration
+local function get_tenant_mail_config(api_key)
+ local mail_config, error_msg = config.get_mail_config_for_api_key(api_key)
+
+ if not mail_config then
+ return nil, error_msg or "No mail configuration found for API key"
+ end
+
+ -- Validate essential mail configuration
+ if not mail_config.to_address then
+ return nil, "No recipient configured for this API key"
+ end
+
+ if not mail_config.from_address then
+ return nil, "No sender address configured for this API key"
+ end
+
+ return mail_config, nil
+end
+
+-- Multi-Tenant Mail service handler
+function MailRoute.handle_mail_send(request, server)
+ print("Mail endpoint called - Method: " .. request.method .. ", Path: " .. request.path)
+ print("Authenticated as: " .. request.auth.key_name .. " (" .. request.auth.api_key .. ")")
+
+ -- Basic request validation
+ if not request.body or request.body == "" then
+ return {error = "No request body", code = "MISSING_BODY"}
+ end
+
+ -- Parse JSON
+ local success, data = pcall(cjson.decode, request.body)
+ if not success then
+ return {error = "Invalid JSON", body = request.body, code = "INVALID_JSON"}
+ end
+
+ -- Validate mail data
+ local valid, error_message = validate_mail_data(data)
+ if not valid then
+ return {error = error_message, code = "VALIDATION_ERROR"}
+ end
+
+ -- Get tenant-specific mail configuration
+ local tenant_mail_config, config_error = get_tenant_mail_config(request.auth.api_key)
+ if not tenant_mail_config then
+ print("Mail config error for API key " .. request.auth.api_key .. ": " .. config_error)
+ return server:create_response(500, {
+ error = "Mail configuration error: " .. config_error,
+ code = "CONFIG_ERROR"
+ }, nil, nil, request)
+ end
+
+ -- Generate request ID for tracking
+ local request_id = generate_request_id()
+
+ -- Apply tenant-specific subject prefix
+ local subject = data.subject or "Contact Form Message"
+ if tenant_mail_config.subject_prefix and tenant_mail_config.subject_prefix ~= "" then
+ subject = tenant_mail_config.subject_prefix .. subject
+ end
+
+ -- Prepare email content with tenant information
+ local email_content = string.format(
+ "Website: %s (%s)\nFrom: %s <%s>\nSubject: %s\n\n%s\n\n---\nSent via Furt Gateway\nAPI Key: %s\nRequest ID: %s",
+ request.auth.key_name,
+ request.auth.api_key,
+ data.name,
+ data.email,
+ data.subject or "Contact Form Message",
+ data.message,
+ request.auth.key_name,
+ request_id
+ )
+
+ -- Send email via SMTP using tenant-specific configuration
+ local SMTP = require("src.smtp")
+ local smtp_client = SMTP:new(tenant_mail_config)
+
+ print("Sending mail for tenant: " .. request.auth.key_name)
+ print(" To: " .. tenant_mail_config.to_address)
+ print(" From: " .. tenant_mail_config.from_address)
+ print(" SMTP: " .. tenant_mail_config.smtp_server .. ":" .. tenant_mail_config.smtp_port)
+
+ local smtp_success, smtp_result = smtp_client:send_email(
+ tenant_mail_config.to_address,
+ subject,
+ email_content,
+ data.name
+ )
+
+ if smtp_success then
+ -- Success response with tenant information
+ return {
+ success = true,
+ message = "Mail sent successfully",
+ request_id = request_id,
+ tenant = {
+ name = request.auth.key_name,
+ recipient = tenant_mail_config.to_address,
+ smtp_server = tenant_mail_config.smtp_server
+ }
+ }
+ else
+ -- SMTP error - log and return error
+ print("SMTP Error for tenant " .. request.auth.key_name .. ": " .. tostring(smtp_result))
+ return server:create_response(500, {
+ success = false,
+ error = "Failed to send email: " .. tostring(smtp_result),
+ request_id = request_id,
+ tenant = request.auth.key_name,
+ code = "SMTP_ERROR"
+ }, nil, nil, request)
+ end
+end
+
+return MailRoute
+
diff --git a/src/smtp.lua b/src/smtp.lua
new file mode 100644
index 0000000..a3340a8
--- /dev/null
+++ b/src/smtp.lua
@@ -0,0 +1,349 @@
+-- src/smtp.lua
+-- Universal SMTP implementation with SSL compatibility
+-- Supports both luaossl (Arch) and luasec (OpenBSD)
+-- Dragons@Work Digital Sovereignty Project
+
+local socket = require("socket")
+
+local SMTP = {}
+
+-- SSL Compatibility Layer - Auto-detect available SSL library
+local SSLCompat = {}
+
+function SSLCompat:detect_ssl_library()
+ -- Try luaossl first (more feature-complete)
+ local success, ssl_lib = pcall(require, "ssl")
+ if success and ssl_lib and ssl_lib.wrap then
+ -- Check if it's luaossl (has more comprehensive API)
+ if ssl_lib.newcontext or type(ssl_lib.wrap) == "function" then
+ return "luaossl", ssl_lib
+ end
+ end
+
+ -- Try luasec
+ local success, ssl_lib = pcall(require, "ssl")
+ if success and ssl_lib then
+ -- luasec typically has ssl.wrap function but different API
+ if ssl_lib.wrap and not ssl_lib.newcontext then
+ return "luasec", ssl_lib
+ end
+ end
+
+ return nil, "No compatible SSL library found (tried luaossl, luasec)"
+end
+
+function SSLCompat:wrap_socket(sock, options)
+ local ssl_type, ssl_lib = self:detect_ssl_library()
+
+ if not ssl_type then
+ return nil, ssl_lib -- ssl_lib contains error message
+ end
+
+ if ssl_type == "luaossl" then
+ return self:wrap_luaossl(sock, options, ssl_lib)
+ elseif ssl_type == "luasec" then
+ return self:wrap_luasec(sock, options, ssl_lib)
+ end
+
+ return nil, "Unknown SSL library type: " .. ssl_type
+end
+
+function SSLCompat:wrap_luaossl(sock, options, ssl_lib)
+ -- luaossl API
+ local ssl_sock, err = ssl_lib.wrap(sock, {
+ mode = "client",
+ protocol = "tlsv1_2",
+ verify = "none" -- For self-signed certs
+ })
+
+ if not ssl_sock then
+ return nil, "luaossl wrap failed: " .. (err or "unknown error")
+ end
+
+ -- luaossl typically does handshake automatically, but explicit is safer
+ local success, err = pcall(function() return ssl_sock:dohandshake() end)
+ if not success then
+ -- Some luaossl versions don't need explicit handshake
+ -- Continue if dohandshake doesn't exist
+ end
+
+ return ssl_sock, nil
+end
+
+function SSLCompat:wrap_luasec(sock, options, ssl_lib)
+ -- luasec API
+ local ssl_sock, err = ssl_lib.wrap(sock, {
+ protocol = "tlsv1_2",
+ mode = "client",
+ verify = "none",
+ options = "all"
+ })
+
+ if not ssl_sock then
+ return nil, "luasec wrap failed: " .. (err or "unknown error")
+ end
+
+ -- luasec requires explicit handshake
+ local success, err = ssl_sock:dohandshake()
+ if not success then
+ return nil, "luasec handshake failed: " .. (err or "unknown error")
+ end
+
+ return ssl_sock, nil
+end
+
+-- Create SMTP instance
+function SMTP:new(config)
+ local instance = {
+ server = config.smtp_server,
+ port = config.smtp_port,
+ username = config.username,
+ password = config.password,
+ from_address = config.from_address,
+ use_ssl = config.use_ssl or true,
+ debug = config.debug or false,
+ ssl_compat = SSLCompat
+ }
+ setmetatable(instance, self)
+ self.__index = self
+ return instance
+end
+
+-- Base64 encoding for SMTP AUTH
+function SMTP:base64_encode(str)
+ local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+ return ((str:gsub('.', function(x)
+ local r, b = '', x:byte()
+ for i = 8, 1, -1 do
+ r = r .. (b % 2^i - b % 2^(i-1) > 0 and '1' or '0')
+ end
+ return r;
+ end) .. '0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
+ if (#x < 6) then return '' end
+ local c = 0
+ for i = 1, 6 do
+ c = c + (x:sub(i,i) == '1' and 2^(6-i) or 0)
+ end
+ return b:sub(c+1,c+1)
+ end) .. ({ '', '==', '=' })[#str % 3 + 1])
+end
+
+-- Send SMTP command and read response
+function SMTP:send_command(sock, command, expected_code)
+ if self.debug then
+ print("SMTP CMD: " .. (command or ""):gsub("\r\n", "\\r\\n"))
+ end
+
+ -- Only send if command is not nil (for server greeting, command is nil)
+ if command then
+ local success, err = sock:send(command .. "\r\n")
+ if not success then
+ return false, "Failed to send command: " .. (err or "unknown error")
+ end
+ end
+
+ local response, err = sock:receive()
+ if not response then
+ return false, "Failed to receive response: " .. (err or "unknown error")
+ end
+
+ if self.debug then
+ print("SMTP RSP: " .. response)
+ end
+
+ -- Handle multi-line responses (like EHLO)
+ local full_response = response
+ while response:match("^%d%d%d%-") do
+ response, err = sock:receive()
+ if not response then
+ return false, "Failed to receive multi-line response: " .. (err or "unknown error")
+ end
+ if self.debug then
+ print("SMTP RSP: " .. response)
+ end
+ full_response = full_response .. "\n" .. response
+ end
+
+ local code = response:match("^(%d+)")
+ if expected_code and code ~= tostring(expected_code) then
+ return false, "Unexpected response: " .. full_response
+ end
+
+ return true, full_response
+end
+
+-- Connect to SMTP server with universal SSL support
+function SMTP:connect()
+ -- Create socket
+ local sock, err = socket.tcp()
+ if not sock then
+ return false, "Failed to create socket: " .. (err or "unknown error")
+ end
+
+ -- Set timeout
+ sock:settimeout(30)
+
+ -- Connect to server
+ local success, err = sock:connect(self.server, self.port)
+ if not success then
+ return false, "Failed to connect to " .. self.server .. ":" .. self.port .. " - " .. (err or "unknown error")
+ end
+
+ -- Wrap with SSL for port 465 using compatibility layer
+ if self.use_ssl and self.port == 465 then
+ local ssl_sock, err = self.ssl_compat:wrap_socket(sock, {
+ mode = "client",
+ protocol = "tlsv1_2"
+ })
+
+ if not ssl_sock then
+ sock:close()
+ return false, "Failed to establish SSL connection: " .. (err or "unknown error")
+ end
+
+ sock = ssl_sock
+ end
+
+ -- Read server greeting
+ local success, response = self:send_command(sock, nil, 220)
+ if not success then
+ sock:close()
+ return false, "SMTP server greeting failed: " .. response
+ end
+
+ return sock, nil
+end
+
+-- Send email
+function SMTP:send_email(to_address, subject, message, from_name)
+ if not self.username or not self.password then
+ return false, "SMTP username or password not configured"
+ end
+
+ -- Connect to server
+ local sock, err = self:connect()
+ if not sock then
+ return false, err
+ end
+
+ local function cleanup_and_fail(error_msg)
+ sock:close()
+ return false, error_msg
+ end
+
+ -- EHLO command
+ local success, response = self:send_command(sock, "EHLO furt-lua", 250)
+ if not success then
+ return cleanup_and_fail("EHLO failed: " .. response)
+ end
+
+ -- STARTTLS hinzufügen für Port 587
+ if self.port == 587 and self.use_ssl then
+ -- STARTTLS command
+ local success, response = self:send_command(sock, "STARTTLS", 220)
+ if not success then
+ return cleanup_and_fail("STARTTLS failed: " .. response)
+ end
+
+ -- Upgrade connection to SSL
+ local ssl_sock, err = self.ssl_compat:wrap_socket(sock, {
+ mode = "client",
+ protocol = "tlsv1_2"
+ })
+
+ if not ssl_sock then
+ return cleanup_and_fail("SSL upgrade failed: " .. err)
+ end
+
+ sock = ssl_sock
+
+ -- EHLO again over encrypted connection
+ local success, response = self:send_command(sock, "EHLO furt-lua", 250)
+ if not success then
+ return cleanup_and_fail("EHLO after STARTTLS failed: " .. response)
+ end
+ end
+
+ -- AUTH LOGIN
+ local success, response = self:send_command(sock, "AUTH LOGIN", 334)
+ if not success then
+ return cleanup_and_fail("AUTH LOGIN failed: " .. response)
+ end
+
+ -- Send username (base64 encoded)
+ local username_b64 = self:base64_encode(self.username)
+ local success, response = self:send_command(sock, username_b64, 334)
+ if not success then
+ return cleanup_and_fail("Username authentication failed: " .. response)
+ end
+
+ -- Send password (base64 encoded)
+ local password_b64 = self:base64_encode(self.password)
+ local success, response = self:send_command(sock, password_b64, 235)
+ if not success then
+ return cleanup_and_fail("Password authentication failed: " .. response)
+ end
+
+ -- MAIL FROM
+ local mail_from = "MAIL FROM:<" .. self.from_address .. ">"
+ local success, response = self:send_command(sock, mail_from, 250)
+ if not success then
+ return cleanup_and_fail("MAIL FROM failed: " .. response)
+ end
+
+ -- RCPT TO
+ local rcpt_to = "RCPT TO:<" .. to_address .. ">"
+ local success, response = self:send_command(sock, rcpt_to, 250)
+ if not success then
+ return cleanup_and_fail("RCPT TO failed: " .. response)
+ end
+
+ -- DATA command
+ local success, response = self:send_command(sock, "DATA", 354)
+ if not success then
+ return cleanup_and_fail("DATA command failed: " .. response)
+ end
+
+ -- Generate unique Message-ID
+ -- Extract domain from configured from_address
+ local hostname = self.from_address:match("@(.+)") or self.server
+ local message_id = string.format("<%d.%d@%s>", os.time(), math.random(10000), hostname)
+
+ -- Build email message
+ local display_name = from_name or "Furt Contact Form"
+ local email_content = string.format(
+ "From: %s <%s>\r\n" ..
+ "To: <%s>\r\n" ..
+ "Subject: %s\r\n" ..
+ "Date: %s\r\n" ..
+ "Message-ID: %s\r\n" ..
+ "MIME-Version: 1.0\r\n" ..
+ "Content-Type: text/plain; charset=UTF-8\r\n" ..
+ "Content-Transfer-Encoding: 8bit\r\n" ..
+ "\r\n" ..
+ "%s\r\n" ..
+ ".",
+ display_name,
+ self.from_address,
+ to_address,
+ subject,
+ os.date("%a, %d %b %Y %H:%M:%S %z"),
+ message_id,
+ message
+ )
+
+ -- Send email content
+ local success, response = self:send_command(sock, email_content, 250)
+ if not success then
+ return cleanup_and_fail("Email sending failed: " .. response)
+ end
+
+ -- QUIT
+ self:send_command(sock, "QUIT", 221)
+ sock:close()
+
+ return true, "Email sent successfully"
+end
+
+return SMTP
+
diff --git a/tests/test_http.lua b/tests/test_http.lua
new file mode 100644
index 0000000..30dc636
--- /dev/null
+++ b/tests/test_http.lua
@@ -0,0 +1,273 @@
+-- furt-lua/tests/test_http.lua
+-- Basic HTTP tests for Furt Lua HTTP-Server
+
+local socket = require("socket")
+local cjson = require("cjson")
+
+-- Test configuration
+local TEST_HOST = "127.0.0.1"
+local TEST_PORT = 8080
+local TEST_TIMEOUT = 5
+
+-- Test results
+local tests_run = 0
+local tests_passed = 0
+local tests_failed = 0
+
+-- ANSI colors
+local GREEN = "\27[32m"
+local RED = "\27[31m"
+local YELLOW = "\27[33m"
+local RESET = "\27[0m"
+
+-- Test helper functions
+local function log(level, message)
+ local prefix = {
+ INFO = YELLOW .. "[INFO]" .. RESET,
+ PASS = GREEN .. "[PASS]" .. RESET,
+ FAIL = RED .. "[FAIL]" .. RESET
+ }
+ print(prefix[level] .. " " .. message)
+end
+
+local function http_request(method, path, body, headers)
+ local client = socket.connect(TEST_HOST, TEST_PORT)
+ if not client then
+ return nil, "Connection failed"
+ end
+
+ client:settimeout(TEST_TIMEOUT)
+
+ -- Build request
+ headers = headers or {}
+ local request_lines = {method .. " " .. path .. " HTTP/1.1"}
+
+ -- Add headers
+ table.insert(request_lines, "Host: " .. TEST_HOST .. ":" .. TEST_PORT)
+ if body then
+ table.insert(request_lines, "Content-Length: " .. #body)
+ table.insert(request_lines, "Content-Type: application/json")
+ end
+
+ for key, value in pairs(headers) do
+ table.insert(request_lines, key .. ": " .. value)
+ end
+
+ table.insert(request_lines, "") -- Empty line
+
+ local request = table.concat(request_lines, "\r\n")
+ if body then
+ request = request .. body
+ end
+
+ -- Send request
+ local success, err = client:send(request)
+ if not success then
+ client:close()
+ return nil, "Send failed: " .. (err or "unknown")
+ end
+
+ -- Read response
+ local response_line = client:receive()
+ if not response_line then
+ client:close()
+ return nil, "No response received"
+ end
+
+ -- Parse status
+ local status = response_line:match("HTTP/1%.1 (%d+)")
+ status = tonumber(status)
+
+ -- Read headers
+ local response_headers = {}
+ local content_length = 0
+
+ while true do
+ local line = client:receive()
+ if not line or line == "" then
+ break
+ end
+
+ local key, value = line:match("([^:]+): (.+)")
+ if key and value then
+ response_headers[key:lower()] = value
+ if key:lower() == "content-length" then
+ content_length = tonumber(value) or 0
+ end
+ end
+ end
+
+ -- Read body
+ local response_body = ""
+ if content_length > 0 then
+ response_body = client:receive(content_length) or ""
+ end
+
+ client:close()
+
+ return {
+ status = status,
+ headers = response_headers,
+ body = response_body
+ }
+end
+
+local function assert_equal(actual, expected, message)
+ tests_run = tests_run + 1
+ if actual == expected then
+ tests_passed = tests_passed + 1
+ log("PASS", message)
+ return true
+ else
+ tests_failed = tests_failed + 1
+ log("FAIL", message .. " (expected: " .. tostring(expected) .. ", got: " .. tostring(actual) .. ")")
+ return false
+ end
+end
+
+local function assert_status(response, expected_status, test_name)
+ return assert_equal(response and response.status, expected_status,
+ test_name .. " - Status Code")
+end
+
+-- Test functions
+local function test_health_check()
+ log("INFO", "Testing health check endpoint...")
+
+ local response = http_request("GET", "/health")
+ if not response then
+ log("FAIL", "Health check - No response")
+ tests_run = tests_run + 1
+ tests_failed = tests_failed + 1
+ return
+ end
+
+ assert_status(response, 200, "Health check")
+
+ if response.body then
+ local success, data = pcall(cjson.decode, response.body)
+ if success then
+ assert_equal(data.status, "healthy", "Health check - Status field")
+ assert_equal(data.service, "furt-lua", "Health check - Service field")
+ else
+ log("FAIL", "Health check - Invalid JSON response")
+ tests_run = tests_run + 1
+ tests_failed = tests_failed + 1
+ end
+ end
+end
+
+local function test_basic_post()
+ log("INFO", "Testing basic POST endpoint...")
+
+ local test_data = {test = "data", number = 42}
+ local response = http_request("POST", "/test", cjson.encode(test_data))
+
+ if not response then
+ log("FAIL", "Basic POST - No response")
+ tests_run = tests_run + 1
+ tests_failed = tests_failed + 1
+ return
+ end
+
+ assert_status(response, 200, "Basic POST")
+
+ if response.body then
+ local success, data = pcall(cjson.decode, response.body)
+ if success then
+ assert_equal(data.message, "Test endpoint working", "Basic POST - Message field")
+ else
+ log("FAIL", "Basic POST - Invalid JSON response")
+ tests_run = tests_run + 1
+ tests_failed = tests_failed + 1
+ end
+ end
+end
+
+local function test_mail_endpoint()
+ log("INFO", "Testing mail endpoint...")
+
+ -- Test with valid data
+ local mail_data = {
+ name = "Test User",
+ email = "test@example.com",
+ message = "This is a test message"
+ }
+
+ local response = http_request("POST", "/v1/mail/send", cjson.encode(mail_data))
+
+ if not response then
+ log("FAIL", "Mail endpoint - No response")
+ tests_run = tests_run + 1
+ tests_failed = tests_failed + 1
+ return
+ end
+
+ assert_status(response, 200, "Mail endpoint - Valid data")
+
+ -- Test with invalid data (missing fields)
+ local invalid_data = {name = "Test"}
+ local response2 = http_request("POST", "/v1/mail/send", cjson.encode(invalid_data))
+
+ assert_status(response2, 400, "Mail endpoint - Invalid data")
+
+ -- Test with no body
+ local response3 = http_request("POST", "/v1/mail/send")
+ assert_status(response3, 400, "Mail endpoint - No body")
+end
+
+local function test_404_handling()
+ log("INFO", "Testing 404 handling...")
+
+ local response = http_request("GET", "/nonexistent")
+ assert_status(response, 404, "404 handling")
+end
+
+-- Main test runner
+local function run_tests()
+ log("INFO", "Starting Furt HTTP-Server tests...")
+ log("INFO", "Target: http://" .. TEST_HOST .. ":" .. TEST_PORT)
+ print("")
+
+ -- Check if server is running
+ local test_response = http_request("GET", "/health")
+ if not test_response then
+ log("FAIL", "Server is not running on " .. TEST_HOST .. ":" .. TEST_PORT)
+ log("INFO", "Start server with: ./scripts/start.sh")
+ return false
+ end
+
+ -- Run tests
+ test_health_check()
+ test_basic_post()
+ test_mail_endpoint()
+ test_404_handling()
+
+ -- Print results
+ print("")
+ log("INFO", "Test Results:")
+ log("INFO", "Tests run: " .. tests_run)
+ log("INFO", "Passed: " .. tests_passed)
+ log("INFO", "Failed: " .. tests_failed)
+
+ if tests_failed == 0 then
+ log("PASS", "All tests passed! 🎉")
+ return true
+ else
+ log("FAIL", tests_failed .. " test(s) failed")
+ return false
+ end
+end
+
+-- Run tests if executed directly
+if arg and arg[0] and arg[0]:match("test_http%.lua$") then
+ local success = run_tests()
+ os.exit(success and 0 or 1)
+end
+
+-- Export for use as module
+return {
+ run_tests = run_tests,
+ http_request = http_request
+}
+