feat(config): implement multi-tenant config system (DAW/furt#89)
- nginx-style furt.conf configuration - Multi-tenant mail routing per API key - Custom SMTP support per customer - Backward compatibility via server.lua adapter WIP: Ready for testing on werner
This commit is contained in:
parent
be3b9614d0
commit
3ed921312f
5 changed files with 625 additions and 86 deletions
90
config/furt.conf.example
Normal file
90
config/furt.conf.example
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# furt.conf - Multi-Tenant Configuration Example
|
||||||
|
# Dragons@Work Digital Sovereignty Project
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
|
[server]
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = 8080
|
||||||
|
log_level = info
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
@ -1,25 +1,30 @@
|
||||||
-- config/server.lua
|
-- config/server.lua
|
||||||
-- Server configuration for Furt Lua HTTP-Server
|
-- Multi-Tenant server configuration using nginx-style config parser
|
||||||
|
-- Dragons@Work Digital Sovereignty Project
|
||||||
|
|
||||||
return {
|
local ConfigParser = require("src.config_parser")
|
||||||
-- HTTP Server settings
|
|
||||||
host = "127.0.0.1",
|
|
||||||
port = 8080,
|
|
||||||
|
|
||||||
-- Timeouts (seconds)
|
-- Load configuration from furt.conf
|
||||||
client_timeout = 10,
|
local config = ConfigParser.load_config()
|
||||||
|
|
||||||
|
-- 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
|
-- CORS Configuration
|
||||||
cors = {
|
cors = {
|
||||||
-- Default allowed origins for development
|
|
||||||
-- Override in production with CORS_ALLOWED_ORIGINS environment variable
|
|
||||||
allowed_origins = (function()
|
allowed_origins = (function()
|
||||||
local env_origins = os.getenv("CORS_ALLOWED_ORIGINS")
|
local env_origins = os.getenv("CORS_ALLOWED_ORIGINS")
|
||||||
if env_origins then
|
if env_origins then
|
||||||
-- Parse comma-separated list from environment
|
-- Parse comma-separated list from environment
|
||||||
local origins = {}
|
local origins = {}
|
||||||
for origin in env_origins:gmatch("([^,]+)") do
|
for origin in env_origins:gmatch("([^,]+)") do
|
||||||
table.insert(origins, origin:match("^%s*(.-)%s*$")) -- trim whitespace
|
table.insert(origins, origin:match("^%s*(.-)%s*$"))
|
||||||
end
|
end
|
||||||
return origins
|
return origins
|
||||||
else
|
else
|
||||||
|
|
@ -35,54 +40,40 @@ return {
|
||||||
},
|
},
|
||||||
|
|
||||||
-- Logging
|
-- Logging
|
||||||
log_level = "info",
|
log_level = config.server.log_level or "info",
|
||||||
log_requests = true,
|
log_requests = config.server.log_requests or true,
|
||||||
|
|
||||||
-- API-Key-Authentifizierung (PRODUCTION READY)
|
-- API Keys (converted from nginx-style to old format for backward compatibility)
|
||||||
api_keys = {
|
api_keys = config.api_keys,
|
||||||
-- Hugo Frontend API-Key (für Website-Formulare)
|
|
||||||
[os.getenv("HUGO_API_KEY") or "hugo-dev-key-change-in-production"] = {
|
|
||||||
name = "Hugo Frontend",
|
|
||||||
permissions = {"mail:send"},
|
|
||||||
allowed_ips = {
|
|
||||||
"127.0.0.1", -- Localhost
|
|
||||||
"10.0.0.0/8", -- Private network
|
|
||||||
"192.168.0.0/16", -- Private network
|
|
||||||
"172.16.0.0/12" -- Private network
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
-- Admin API-Key (für Testing und Management)
|
-- Default SMTP config (for legacy compatibility)
|
||||||
[os.getenv("ADMIN_API_KEY") or "admin-dev-key-change-in-production"] = {
|
mail = config.smtp_default,
|
||||||
name = "Admin Access",
|
|
||||||
permissions = {"*"}, -- All permissions
|
|
||||||
allowed_ips = {
|
|
||||||
"127.0.0.1", -- Local only for admin
|
|
||||||
"10.0.0.0/8" -- Internal network
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
-- Optional: Monitoring API-Key (nur Health-Checks)
|
-- Multi-tenant mail configuration function
|
||||||
[os.getenv("MONITORING_API_KEY") or "monitoring-dev-key"] = {
|
get_mail_config_for_api_key = function(api_key)
|
||||||
name = "Monitoring Service",
|
return ConfigParser.get_mail_config_for_api_key(config, api_key)
|
||||||
permissions = {"monitoring:health"},
|
end,
|
||||||
allowed_ips = {
|
|
||||||
"127.0.0.1",
|
|
||||||
"10.0.0.0/8",
|
|
||||||
"172.16.0.0/12"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
-- Mail configuration (for SMTP integration)
|
-- Raw config access (for advanced usage)
|
||||||
mail = {
|
raw_config = config
|
||||||
smtp_server = os.getenv("SMTP_HOST") or "mail.example.org",
|
|
||||||
smtp_port = tonumber(os.getenv("SMTP_PORT")) or 465,
|
|
||||||
use_ssl = true,
|
|
||||||
username = os.getenv("SMTP_USERNAME"),
|
|
||||||
password = os.getenv("SMTP_PASSWORD"),
|
|
||||||
from_address = os.getenv("SMTP_FROM") or "noreply@example.org",
|
|
||||||
to_address = os.getenv("SMTP_TO") or "admin@example.org"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- 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(" Default SMTP: " .. (config.smtp_default.host or "not configured"))
|
||||||
|
|
||||||
|
local api_key_count = 0
|
||||||
|
for key_name, key_config in pairs(config.api_keys) do
|
||||||
|
api_key_count = api_key_count + 1
|
||||||
|
local smtp_info = ""
|
||||||
|
if key_config.mail_smtp_host then
|
||||||
|
smtp_info = " (custom SMTP: " .. key_config.mail_smtp_host .. ")"
|
||||||
|
end
|
||||||
|
print(" API Key: " .. key_config.name .. " -> " .. key_config.mail_to .. smtp_info)
|
||||||
|
end
|
||||||
|
print(" Total API Keys: " .. api_key_count)
|
||||||
|
|
||||||
|
return server_config
|
||||||
|
|
||||||
|
|
|
||||||
176
docs/setup-guide.md
Normal file
176
docs/setup-guide.md
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
# Multi-Tenant furt Setup-Anleitung
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Dateien platzieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# OpenBSD/FreeBSD
|
||||||
|
mkdir -p /usr/local/etc/furt
|
||||||
|
mkdir -p /usr/local/share/furt
|
||||||
|
|
||||||
|
# Oder Linux
|
||||||
|
mkdir -p /etc/furt
|
||||||
|
mkdir -p /usr/local/share/furt
|
||||||
|
|
||||||
|
# Source code
|
||||||
|
cp -r src/ /usr/local/share/furt/
|
||||||
|
cp -r config/ /usr/local/share/furt/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Konfiguration erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Beispiel-Config kopieren und anpassen
|
||||||
|
# OpenBSD/FreeBSD:
|
||||||
|
cp furt.conf.example /usr/local/etc/furt/furt.conf
|
||||||
|
|
||||||
|
# Linux:
|
||||||
|
cp furt.conf.example /etc/furt/furt.conf
|
||||||
|
|
||||||
|
# Config editieren
|
||||||
|
vi /usr/local/etc/furt/furt.conf # oder /etc/furt/furt.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start-Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/sh
|
||||||
|
# /usr/local/bin/furt
|
||||||
|
|
||||||
|
cd /usr/local/share/furt
|
||||||
|
lua src/main.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Tenant Konfiguration
|
||||||
|
|
||||||
|
### Beispiel für 3 Websites
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[server]
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = 8080
|
||||||
|
|
||||||
|
[smtp_default]
|
||||||
|
host = mail.dragons-at-work.de
|
||||||
|
port = 465
|
||||||
|
user = noreply@dragons-at-work.de
|
||||||
|
password = your-smtp-password
|
||||||
|
|
||||||
|
# Website 1: Dragons@Work
|
||||||
|
[api_key "daw-key-abc123"]
|
||||||
|
name = "Dragons@Work Website"
|
||||||
|
permissions = mail:send
|
||||||
|
allowed_ips = 1.2.3.4/32, 10.0.0.0/8
|
||||||
|
mail_to = admin@dragons-at-work.de
|
||||||
|
mail_from = noreply@dragons-at-work.de
|
||||||
|
mail_subject_prefix = "[DAW] "
|
||||||
|
|
||||||
|
# Website 2: Biocodie (gleiche SMTP, andere Empfänger)
|
||||||
|
[api_key "bio-key-def456"]
|
||||||
|
name = "Biocodie Website"
|
||||||
|
permissions = mail:send
|
||||||
|
allowed_ips = 5.6.7.8/32
|
||||||
|
mail_to = contact@biocodie.de
|
||||||
|
mail_from = noreply@biocodie.de
|
||||||
|
mail_subject_prefix = "[Biocodie] "
|
||||||
|
|
||||||
|
# Website 3: Kunde mit eigenem SMTP
|
||||||
|
[api_key "kunde-key-ghi789"]
|
||||||
|
name = "Kunde X Website"
|
||||||
|
permissions = mail:send
|
||||||
|
allowed_ips = 9.10.11.12/32
|
||||||
|
mail_to = info@kunde-x.de
|
||||||
|
mail_from = noreply@kunde-x.de
|
||||||
|
mail_smtp_host = mail.kunde-x.de
|
||||||
|
mail_smtp_user = noreply@kunde-x.de
|
||||||
|
mail_smtp_pass = kunde-smtp-password
|
||||||
|
```
|
||||||
|
|
||||||
|
## Admin-Workflow
|
||||||
|
|
||||||
|
### Neue Website hinzufügen
|
||||||
|
|
||||||
|
1. **Config editieren:**
|
||||||
|
```bash
|
||||||
|
vi /usr/local/etc/furt/furt.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Neuen API-Key-Block hinzufügen:**
|
||||||
|
```ini
|
||||||
|
[api_key "neue-website-key"]
|
||||||
|
name = "Neue Website"
|
||||||
|
permissions = mail:send
|
||||||
|
allowed_ips = 12.34.56.78/32
|
||||||
|
mail_to = contact@neue-website.de
|
||||||
|
mail_from = noreply@neue-website.de
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **furt neu starten:**
|
||||||
|
```bash
|
||||||
|
systemctl restart furt
|
||||||
|
# oder
|
||||||
|
pkill -f "lua.*main.lua" && /usr/local/bin/furt &
|
||||||
|
```
|
||||||
|
|
||||||
|
### Website testen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test mit curl
|
||||||
|
curl -X POST http://localhost:8080/v1/mail/send \
|
||||||
|
-H "X-API-Key: neue-website-key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"subject": "Test Message",
|
||||||
|
"message": "This is a test message"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vorteile des Multi-Tenant-Systems
|
||||||
|
|
||||||
|
### ✅ Ein Server, viele Websites
|
||||||
|
- Alle Websites nutzen eine furt-Instanz
|
||||||
|
- Jede Website hat eigenen API-Key
|
||||||
|
- Verschiedene Empfänger-Adressen
|
||||||
|
- Verschiedene SMTP-Server möglich
|
||||||
|
|
||||||
|
### ✅ Admin-freundlich
|
||||||
|
- Nginx-style Config-Format
|
||||||
|
- Einfach neue Websites hinzufügen
|
||||||
|
- Klare Struktur pro Website
|
||||||
|
- Kommentare möglich
|
||||||
|
|
||||||
|
### ✅ Sicher
|
||||||
|
- IP-Restrictions pro Website
|
||||||
|
- Permissions pro API-Key
|
||||||
|
- Separate SMTP-Credentials möglich
|
||||||
|
- Rate-Limiting bleibt erhalten
|
||||||
|
|
||||||
|
### ✅ Flexibel
|
||||||
|
- Default SMTP + website-spezifische SMTP
|
||||||
|
- Subject-Prefix pro Website
|
||||||
|
- Verschiedene Mail-Adressen
|
||||||
|
- Beliebig viele Websites
|
||||||
|
|
||||||
|
## Backward Compatibility
|
||||||
|
|
||||||
|
Das neue System ist **vollständig kompatibel** mit der alten config/server.lua API. Bestehende Module (auth.lua, main.lua, etc.) funktionieren ohne Änderungen.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Config-Parsing-Fehler
|
||||||
|
```bash
|
||||||
|
# Config-Syntax prüfen
|
||||||
|
lua -e "require('src.config_parser').parse_file('/usr/local/etc/furt/furt.conf')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mail-Routing testen
|
||||||
|
```bash
|
||||||
|
# Logs anschauen
|
||||||
|
tail -f /var/log/furt.log
|
||||||
|
|
||||||
|
# Debug-Mode
|
||||||
|
FURT_DEBUG=true lua src/main.lua
|
||||||
|
```
|
||||||
229
src/config_parser.lua
Normal file
229
src/config_parser.lua
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
-- src/config_parser.lua
|
||||||
|
-- nginx-style configuration parser for Multi-Tenant setup
|
||||||
|
-- Dragons@Work Digital Sovereignty Project
|
||||||
|
|
||||||
|
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 line == "" or line:match("^#") then
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
::continue::
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
-- 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/FreeBSD
|
||||||
|
"/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
|
||||||
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
-- furt-lua/src/routes/mail.lua
|
-- src/routes/mail.lua
|
||||||
-- Mail service route handler
|
-- Multi-Tenant Mail service route handler
|
||||||
|
-- API-Key determines mail configuration and recipient
|
||||||
-- Dragons@Work Digital Sovereignty Project
|
-- Dragons@Work Digital Sovereignty Project
|
||||||
|
|
||||||
local cjson = require("cjson")
|
local cjson = require("cjson")
|
||||||
|
|
@ -19,25 +20,25 @@ local function validate_mail_data(data)
|
||||||
if not data.name or type(data.name) ~= "string" or data.name:match("^%s*$") then
|
if not data.name or type(data.name) ~= "string" or data.name:match("^%s*$") then
|
||||||
return false, "Name is required and cannot be empty"
|
return false, "Name is required and cannot be empty"
|
||||||
end
|
end
|
||||||
|
|
||||||
if not data.email or not validate_email(data.email) then
|
if not data.email or not validate_email(data.email) then
|
||||||
return false, "Valid email address is required"
|
return false, "Valid email address is required"
|
||||||
end
|
end
|
||||||
|
|
||||||
if not data.message or type(data.message) ~= "string" or data.message:match("^%s*$") then
|
if not data.message or type(data.message) ~= "string" or data.message:match("^%s*$") then
|
||||||
return false, "Message is required and cannot be empty"
|
return false, "Message is required and cannot be empty"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Optional subject validation
|
-- Optional subject validation
|
||||||
if data.subject and (type(data.subject) ~= "string" or #data.subject > 200) then
|
if data.subject and (type(data.subject) ~= "string" or #data.subject > 200) then
|
||||||
return false, "Subject must be a string with max 200 characters"
|
return false, "Subject must be a string with max 200 characters"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Message length validation
|
-- Message length validation
|
||||||
if #data.message > 5000 then
|
if #data.message > 5000 then
|
||||||
return false, "Message too long (max 5000 characters)"
|
return false, "Message too long (max 5000 characters)"
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -46,64 +47,116 @@ local function generate_request_id()
|
||||||
return os.time() .. "-" .. math.random(1000, 9999)
|
return os.time() .. "-" .. math.random(1000, 9999)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Mail service handler
|
-- 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)
|
function MailRoute.handle_mail_send(request, server)
|
||||||
print("Mail endpoint called - Method: " .. request.method .. ", Path: " .. request.path)
|
print("Mail endpoint called - Method: " .. request.method .. ", Path: " .. request.path)
|
||||||
print("Authenticated as: " .. request.auth.key_name .. " (" .. request.auth.api_key .. ")")
|
print("Authenticated as: " .. request.auth.key_name .. " (" .. request.auth.api_key .. ")")
|
||||||
|
|
||||||
-- Basic request validation
|
-- Basic request validation
|
||||||
if not request.body or request.body == "" then
|
if not request.body or request.body == "" then
|
||||||
return {error = "No request body", code = "MISSING_BODY"}
|
return {error = "No request body", code = "MISSING_BODY"}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Parse JSON
|
-- Parse JSON
|
||||||
local success, data = pcall(cjson.decode, request.body)
|
local success, data = pcall(cjson.decode, request.body)
|
||||||
if not success then
|
if not success then
|
||||||
return {error = "Invalid JSON", body = request.body, code = "INVALID_JSON"}
|
return {error = "Invalid JSON", body = request.body, code = "INVALID_JSON"}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Validate mail data
|
-- Validate mail data
|
||||||
local valid, error_message = validate_mail_data(data)
|
local valid, error_message = validate_mail_data(data)
|
||||||
if not valid then
|
if not valid then
|
||||||
return {error = error_message, code = "VALIDATION_ERROR"}
|
return {error = error_message, code = "VALIDATION_ERROR"}
|
||||||
end
|
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
|
-- Generate request ID for tracking
|
||||||
local request_id = generate_request_id()
|
local request_id = generate_request_id()
|
||||||
|
|
||||||
-- Prepare email content
|
-- Apply tenant-specific subject prefix
|
||||||
local subject = data.subject or "Contact Form Message"
|
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(
|
local email_content = string.format(
|
||||||
"From: %s <%s>\nSubject: %s\n\n%s",
|
"Website: %s (%s)\nFrom: %s <%s>\nSubject: %s\n\n%s\n\n---\nSent via Furt Gateway\nAPI Key: %s\nRequest ID: %s",
|
||||||
data.name, data.email, subject, data.message
|
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
|
-- Send email via SMTP using tenant-specific configuration
|
||||||
local SMTP = require("src.smtp")
|
local SMTP = require("src.smtp")
|
||||||
local smtp_client = SMTP:new(config.mail)
|
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(
|
local smtp_success, smtp_result = smtp_client:send_email(
|
||||||
config.mail.to_address,
|
tenant_mail_config.to_address,
|
||||||
subject,
|
subject,
|
||||||
email_content,
|
email_content,
|
||||||
data.name
|
data.name
|
||||||
)
|
)
|
||||||
|
|
||||||
if smtp_success then
|
if smtp_success then
|
||||||
-- Success response
|
-- Success response with tenant information
|
||||||
return {
|
return {
|
||||||
success = true,
|
success = true,
|
||||||
message = "Mail sent successfully",
|
message = "Mail sent successfully",
|
||||||
request_id = request_id,
|
request_id = request_id,
|
||||||
api_key_name = request.auth.key_name
|
tenant = {
|
||||||
|
name = request.auth.key_name,
|
||||||
|
recipient = tenant_mail_config.to_address,
|
||||||
|
smtp_server = tenant_mail_config.smtp_server
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
-- SMTP error - log and return error
|
-- SMTP error - log and return error
|
||||||
print("SMTP Error: " .. tostring(smtp_result))
|
print("SMTP Error for tenant " .. request.auth.key_name .. ": " .. tostring(smtp_result))
|
||||||
return server:create_response(500, {
|
return server:create_response(500, {
|
||||||
success = false,
|
success = false,
|
||||||
error = "Failed to send email: " .. tostring(smtp_result),
|
error = "Failed to send email: " .. tostring(smtp_result),
|
||||||
request_id = request_id,
|
request_id = request_id,
|
||||||
|
tenant = request.auth.key_name,
|
||||||
code = "SMTP_ERROR"
|
code = "SMTP_ERROR"
|
||||||
}, nil, nil, request)
|
}, nil, nil, request)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue