Compare commits

...

89 commits

Author SHA1 Message Date
83e267a608 chore: merkwerk auto-update 2025-09-10 20:04:19 +02:00
f684ea1b4c bump version to 0.1.4 2025-09-10 20:04:19 +02:00
caeb5662d4 chore: merkwerk auto-update 2025-09-10 20:01:19 +02:00
f20915ff33 fix(smtp): add missing headers to prevent spam classification
Add required SMTP headers to fix spam classification issues:
- Message-ID: generated from timestamp and from_address domain
- MIME-Version: 1.0 header for proper email formatting
- Content-Transfer-Encoding: 8bit for UTF-8 content

Fixes rspamd spam score from 10.42/10.00 (reject) to 4.80/10.00 (clean)
by resolving MISSING_MID (-2.50), MISSING_MIME_VERSION (-2.00),
and R_BAD_CTE_7BIT (-1.05) penalties.

Tested with mail-tester.com (10/10 score) and production deployment
on tiamat shows successful delivery to inbox instead of spam folder.

Related DAW/infrastruktur#35
2025-09-10 20:00:34 +02:00
4af068e15c chore: merkwerk auto-update 2025-09-10 16:46:13 +02:00
7a921dc791 Release v0.1.3: Add STARTTLS support for port 587 2025-09-10 16:46:12 +02:00
ec7086259e chore: merkwerk auto-update 2025-09-10 16:45:13 +02:00
304b010a56 fix(smtp): add STARTTLS support for port 587
- Add STARTTLS handshake after EHLO for port 587
- Upgrade socket to SSL after STARTTLS command
- Perform second EHLO over encrypted connection
- Resolves authentication issues with Hetzner and other SMTP providers
- Fixes 'Must issue a STARTTLS command first' error

Closes #113
2025-09-10 16:45:12 +02:00
9cd8f4bce0 chore: merkwerk auto-update 2025-09-10 14:27:55 +02:00
f5d9f359de Release v0.1.2: Complete API Gateway with Multi-Tenant Mail 2025-09-10 14:27:54 +02:00
166325b133 chore: merkwerk auto-update 2025-09-10 12:20:50 +02:00
8b7806670c docs: simplify README and remove FreeBSD support
- Strip README to essentials with wiki references only
- Remove non-existent API docs and troubleshooting links
- Focus on quick start and actual integrations (merkwerk)
- Remove FreeBSD support from all installation scripts
- Clean up platform detection logic in scripts
- Maintain OpenBSD and Linux support only

Reduces maintenance burden and aligns with actual project scope.
2025-09-10 12:20:41 +02:00
6c60d88f62 Merge branch 'security/sanitize-test-scripts' 2025-09-07 21:26:52 +02:00
54c594e656 chore: merkwerk auto-update 2025-09-07 21:25:38 +02:00
08b49d3d75 security: sanitize internal infrastructure details from open source package
- Remove production_test_sequence.sh (DAW-specific production tests)
- Remove setup_env.sh (obsolete .env setup, replaced by furt.conf)
- Sanitize test scripts: replace dragons-at-work.de with example.com
- Sanitize API keys: replace dev keys with placeholder values
- Remove hardcoded DAW fallbacks from http_server.lua and smtp.lua
- Update .gitignore to exclude production-specific test files

Tests remain functional for developers with example domains.
All internal DAW infrastructure details removed from package.

Closes #101
2025-09-07 21:25:25 +02:00
baa2490bbe feat(security): systemd service hardening implementation
Merge feature/systemd-hardening

This merge introduces production-ready security hardening for the
systemd service with 6 pragmatic security options:

- ProtectSystem=strict for read-only filesystem
- ReadWritePaths for required directories only
- ProtectHome=yes to block home directory access
- NoNewPrivileges=yes to prevent privilege escalation
- PrivateTmp=yes for isolated temporary space
- RestrictAddressFamilies=AF_INET for IPv4-only networking

Testing completed successfully on:
- Debian 12 (systemd 247)
- Arch Linux (systemd 256)

No performance impact observed (812K RAM usage).

Closes DAW/furt#110
2025-09-07 19:11:37 +02:00
32c51e326e chore: merkwerk auto-update 2025-09-07 18:40:47 +02:00
24bd94dec4 feat(deployment): add systemd security hardening
- Add ProtectSystem=strict for read-only filesystem
- Add ReadWritePaths for required directories
- Add ProtectHome=yes to block home access
- Add NoNewPrivileges=yes to prevent escalation
- Add PrivateTmp=yes for isolated temp space
- Add RestrictAddressFamilies=AF_INET for IPv4-only

Related DAW/furt#110
2025-09-07 18:40:32 +02:00
77b9685231 Merge branch 'fix/validate-config-posix-regex' 2025-09-07 18:05:30 +02:00
b4bc104750 chore: merkwerk auto-update 2025-09-07 18:00:48 +02:00
683d6e5e5d fix(scripts): resolve POSIX regex compatibility in validate-config.sh
- Replace \s* with [ \t]* for POSIX-compatible whitespace matching
- Addresses false positive 'server port not configured' error
- Ensures validation works correctly across all POSIX-compliant systems

Related to DAW/furt#111
2025-09-07 18:00:41 +02:00
df1edf3dc5 feat(service): merge PID-file based service management (#100)
This merge introduces reliable cross-platform service detection using
PID-files instead of fragile pexp patterns, resolving rcctl check
issues on OpenBSD.

Key improvements:
- PID-file creation in /var/run/furt/ with proper permissions
- Updated start.sh for service vs interactive mode detection
- Fixed OpenBSD rc.d script with PID-file based rc_check()
- Corrected systemd service PIDFile parameter
- Enhanced setup-directories.sh for PID directory creation

Tested successfully on werner (OpenBSD):
- rcctl check furt now shows (ok) instead of (failed)
- Service start/stop/restart works reliably
- PID-file management handles permissions correctly

Closes #100
2025-09-07 17:41:33 +02:00
bbbbeef072 chore: merkwerk auto-update 2025-09-07 16:58:01 +02:00
59f372f2b0 feat(service): implement PID-file based service management
- Add PID directory creation in setup-directories.sh
- Update start.sh to use /var/run/furt/furt.pid for both platforms
- Fix OpenBSD rc.d script pidfile variable path
- Correct systemd service PIDFile parameter path
- Resolve rcctl check detection issues on OpenBSD

Fixes service detection problems where rcctl check would show (failed)
even when service was running. PID-file approach provides reliable
cross-platform service status detection instead of fragile pexp patterns.
Related DAW/furt#100
2025-09-07 16:57:35 +02:00
7ee990b052 chore: merkwerk auto-update 2025-09-05 22:30:13 +02:00
25a709ebbe feat(service): implement PID-file based service management (DAW/furt#100)
- Replace unreliable pexp patterns with PID-file approach
- Add graceful shutdown with timeout handling in rc.d script
- Implement process validation after startup
- Add SIGHUP config reload support for Unix services
- Ensure PID-file cleanup on service exit
- Update systemd service to use PIDFile parameter

Platform improvements:
- OpenBSD: rc_check/rc_stop functions now PID-file based
- Linux: systemd Type=forking with proper PIDFile support
- Cross-platform: /var/run/furt.pid standard location

Resolves service status detection issues where rcctl check showed
(failed) despite running service due to process name variations
across platforms.
2025-09-05 22:30:07 +02:00
ddbb232de2 Merge branch 'refactor/extract-health-routes-and-server-core' 2025-09-05 19:26:37 +02:00
dccf3e462a chore: merkwerk auto-update 2025-09-05 19:25:09 +02:00
d271b846ad refactor: extract health routes and HTTP server core from main.lua
- Extract health routes to src/routes/health.lua (80 lines)
- Extract HTTP server core to src/http_server.lua (256 lines)
- Reduce main.lua to pure orchestration (342 → 27 lines)
- Preserve all functionality and API compatibility
- Add proper module separation following existing patterns
- Enable future service self-registration architecture

Closes #96
2025-09-05 19:25:02 +02:00
1162dceef8 Merge branch 'fix/ssl-dependency-check' 2025-09-05 18:20:08 +02:00
ed7d069953 chore: merkwerk auto-update 2025-09-05 18:20:08 +02:00
d4fa6e34e2 fix(deps): add comprehensive SSL dependency check and unify error messages
- Add SSL/TLS library validation before startup
- Unify all dependency error messages with install instructions
- Provide platform-specific package names for all dependencies
- Prevents silent SMTP SSL failures at runtime

Fixes #109
2025-09-05 18:20:08 +02:00
bf41726613 Merge branch 'fix/json-library-compatibility' 2025-09-05 17:44:42 +02:00
0592381e5d chore: merkwerk auto-update 2025-09-05 17:44:42 +02:00
78e8dedf8e fix(json): add multi-platform JSON library compatibility
- Add flexible JSON detection (cjson preferred, dkjson fallback)
- Update main.lua and mail.lua with found_cjson detection
- Update start.sh to check both JSON libraries
- Enables furt to run on Arch Linux without manual patches
- Maintains API compatibility with existing cjson usage

Fixes #108
2025-09-05 17:44:42 +02:00
785283950f Merge branch 'fix/config-path-consistency' 2025-09-05 17:21:36 +02:00
56b5c43e98 chore: merkwerk auto-update 2025-09-05 17:21:25 +02:00
c15b01a0a6 fix(config): unify config path detection across all scripts
- Add platform detection to start.sh for consistent config paths
- BSD systems: /usr/local/etc/furt/furt.conf
- Linux systems: /etc/furt/furt.conf
- Now consistent with setup-directories.sh and validate-config.sh
- Follows DAW service separation standards

Fixes #103
2025-09-05 17:21:25 +02:00
b068a24ed5 Merge branch 'fix/systemd-type-forking' 2025-09-05 17:03:36 +02:00
7b15a2cfc4 chore: merkwerk auto-update 2025-09-05 17:02:31 +02:00
442b465f16 fix(systemd): use Type=forking for background start.sh compatibility
- Change Type=simple to Type=forking in systemd service
- Properly handle start.sh background process (&)
- Ensures systemd correctly tracks daemon lifecycle
- Fixes BSD-compatible start script integration

Fixes #104
2025-09-05 17:02:31 +02:00
4716630e5b chore: merkwerk auto-update 2025-09-03 22:13:09 +02:00
38a1108a46 feat(deployment): replace monster scripts with modular helper scripts (#87)
- Add install.sh orchestrator with upgrade support
- Add 6 helper scripts (<100 lines each) replacing 700-800 line monsters
- Add deployment/linux/furt.service systemd template
- Support both fresh install and upgrade modes
- Platform-aware detection (OpenBSD/FreeBSD vs Linux)
- Skip user/service creation in upgrade mode
- Preserve existing configuration during updates
- Remove merkwerk dependency from production install script

Helper scripts:
- scripts/setup-user.sh - Create system user (_furt/furt)
- scripts/setup-directories.sh - Create directory structure
- scripts/sync-files.sh - Copy source files to installation
- scripts/create-service.sh - Create system service from templates
- scripts/validate-config.sh - Validate furt.conf syntax
- scripts/health-check.sh - Basic health check functionality

Closes DAW/furt#87
2025-09-03 22:12:58 +02:00
8ad77860d1 chore: merkwerk auto-update 2025-09-03 20:25:19 +02:00
eb64c39312 feat(distribution): add clean package build system
- Add scripts/build-package.sh for production-ready packages
- VCS-agnostic archive creation (git/hg/bzr/fossil support)
- Automatic version detection from VERSION file or git tags
- Secure exclusions for development files and secrets
- Package validation and content verification
- Support for explicit version override

Creates dist/furt-api-gateway-vX.Y.Z.tar.gz with clean structure
for deployment without development dependencies.

Related to DAW/furt#88
2025-09-03 20:24:59 +02:00
f2ca7a5e1c chore: merkwerk auto-update 2025-09-03 12:23:48 +02:00
53ef8ad427 remove internal files from git tracking 2025-09-03 12:23:47 +02:00
c7e33a85bb chore: merkwerk auto-update 2025-09-03 12:16:05 +02:00
589dccc376 fix(packaging): exclude internal files from packages 2025-09-03 12:15:44 +02:00
32d1371a4f chore: merkwerk auto-update 2025-09-03 11:02:41 +02:00
0c59b273d8 chore(license): switch to ISC license
- Replace existing license with ISC license
- Adopt more permissive and simpler license terms
- Align with dragons@work low-tech philosophy
- Maintain full open source compatibility

ISC license provides maximum freedom with minimal legal complexity,
supporting the project's commitment to digital sovereignty and
uncomplicated technology solutions.
2025-09-03 11:02:32 +02:00
fb29a10035 chore: merkwerk auto-update 2025-09-02 21:45:08 +02:00
cec390ef50 chore: remove obsolete .env.example and add issue #98 reference
- Remove .env.example to prevent configuration confusion
- furt now uses only furt.conf for all configuration
- .env.example was misleading users during installation
- Add comment referencing issue #98 resolution

Eliminates config method ambiguity identified in installation testing.
Closes DAW/furt#98
2025-09-02 21:45:01 +02:00
4834ed7f8d chore: merkwerk auto-update 2025-09-02 21:24:58 +02:00
c575d5eed0 fix(deployment): update OpenBSD rc.d template for current service architecture
- Update daemon path to use scripts/start.sh instead of direct lua execution
- Correct process expression pattern (pexp) for lua process detection
- Align template with installation.md service integration patterns
- Ensure compatibility with current furt directory structure

Fixes service integration issues identified in testing.
Related to DAW/furt#98
2025-09-02 21:24:52 +02:00
3bef171671 Merge branch 'fix/service-detection' 2025-09-02 18:38:33 +02:00
467e525786 chore: merkwerk auto-update 2025-09-02 18:36:07 +02:00
11ceb187b6 fix(service): add service vs interactive detection to start.sh
- Fix hanging rcctl/systemd service starts
- Background mode (&) when no TTY (service context)
- Foreground mode (exec) for interactive usage
- POSIX-compatible detection via [ ! -t 0 ]

Fixes service timeout issues on OpenBSD rcctl and Linux systemd.
Tested on werner - service starts correctly and survives reboots.

Fixes DAW/furt#99
2025-09-02 18:35:00 +02:00
0d39d166b8 chore: merkwerk auto-update 2025-08-29 22:01:45 +02:00
9b19b6a95b fix(scripts): resolve lua51 detection failure and remove obsolete environment system
- Fix variable inconsistency: LUA_CMD -> LUA_COMMAND throughout script
- Remove obsolete .env/environment loading - furt reads furt.conf directly
- Add config check for furt.conf (system or project location)
- Implement robust lua51 detection with fallback to lua5.1
- Support all target distributions: Arch, OpenBSD, Debian, FreeBSD
- Add clear installation instructions for missing dependencies
- Allow custom lua path via LUA_COMMAND variable override

This resolves the lua51 detection regression and simplifies the boot process
by eliminating dual config systems (environment vs furt.conf).

Fixes DAW/furt#91
2025-08-29 22:01:38 +02:00
dfeaca55ae Merge branch 'feature/issue-89-multi-tenant' 2025-08-29 20:06:48 +02:00
ef6b995042 chore: merkwerk auto-update 2025-08-29 20:01:55 +02:00
5c17c86fd4 feat(config): integrate rate limiting and CORS configuration from furt.conf
- Add RateLimiter:configure() function to accept config-based limits
- Integrate security section parameters (rate_limit_api_key_max, ip_max, window)
- Add CORS configuration from config file with environment fallback
- Replace hardcoded rate limiting defaults with configurable values
- Add test endpoint control via config.security.enable_test_endpoint
- Update startup logging to show actual configured rate limits
- Add configuration validation and detailed startup information

Rate limiting now uses values from [security] section instead of hardcoded
defaults. CORS origins prioritize config file over environment variables.

Related to DAW/furt#89
2025-08-29 20:01:47 +02:00
ecd4f68595 chore: merkwerk auto-update 2025-08-28 19:53:42 +02:00
8ec401930c fix(config): lua 5.1 compatibility and multi-tenant validation
- Replace goto statements with if-not pattern for Lua 5.1 compatibility
- Validate mail config only for API keys with mail:send permissions
- Safe display of API key info for monitoring keys without mail config
- Fix health check SMTP detection for new config structure
- Multi-tenant system tested and working on port 7811

Fixes multi-tenant config parsing, validation, and health checks.
Related to DAW/furt#89
2025-08-28 19:53:30 +02:00
a5db9a633f merge: integrate merkwerk binary detection into multi-tenant
- Add universal merkwerk binary detection from main
- Required for furt installations (no install without merkwerk)
- Maintains compatibility with multi-tenant architecture

Merges main branch changes for issue #94 into feature/issue-89-multi-tenant
2025-08-28 18:20:54 +02:00
82da58b358 chore: merkwerk auto-update 2025-08-28 17:34:36 +02:00
95dcdbaebb feat(integration): add universal merkwerk binary detection
- Check development binary (./bin/merkwerk)
- Check installed binary (/usr/local/bin/merkwerk)
- Fallback to PATH lookup (command -v merkwerk)
- Proper error handling for missing binary

Related to DAW/furt#94
2025-08-28 17:34:36 +02:00
6b2da02429 chore: merkwerk auto-update 2025-08-20 06:08:04 +02:00
62ddc17393 feat(integration): production-ready .version_history priority
- Change get_info() priority: .version_history first, merkwerk fallback
- Add read_version_history() for production deployment compatibility
- Works without merkwerk binary (tar.gz deployments)
- Maintains development fallback to merkwerk command

Production-ready: tar.gz deployments work without merkwerk installation.
2025-08-20 06:07:56 +02:00
f2d925ee57 chore: merkwerk auto-update 2025-08-19 21:36:34 +02:00
00b8a18527 feat(health): migrate VERSION file to merkwerk integration (#83)
- Replace read_version() with merkwerk.get_health_info()
- Health endpoint now returns content_hash, vcs_info, source tracking
- Add merkwerk_integrated feature flag
- Enhanced startup logs with content-hash and VCS info
- Maintain backward compatibility with version field
- lua51 compatible integration for OpenBSD deployment

Migration from static VERSION file to dynamic merkwerk version tracking.
Health endpoint now provides rich metadata for debugging and monitoring.

Resolves DAW/furt#83
2025-08-19 21:28:22 +02:00
7053af3c0d feat(core): implement file-based API versioning system (DAW/furt#83)
- Add VERSION file in repository root
- Add read_version() function with error handling
- Update /health endpoint to show file-based version
- Add version display during server startup
- Fallback to ?.?.? when VERSION file unreadable

Enables deployment tracking across dev/test/prod environments
2025-08-15 16:57:33 +02:00
5b851b8bfb fix(devdocs) delete project-tree.txt 2025-08-15 16:22:12 +02:00
3ed921312f 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
2025-08-15 16:18:55 +02:00
be3b9614d0 refactor: clean repository structure for v0.1.0 open source release
- Remove Go artifacts (cmd/, internal/, pkg/, go.mod)
- Move furt-lua/* content to repository root
- Restructure as clean src/, config/, scripts/, tests/ layout
- Rewrite README.md as practical tool documentation
- Remove timeline references and marketing language
- Clean .gitignore from Go-era artifacts
- Update config/server.lua with example.org defaults
- Add .env.production to .gitignore for security

Repository now ready for open source distribution with minimal,
focused structure and generic configuration templates.
close issue DAW/furt#86
2025-08-14 09:36:55 +02:00
87c935379b upd(gitignor) update tools/gitea 2025-07-20 19:34:54 +02:00
901f5eb2d8 feat(auth): implement complete API-key authentication with modular architecture (#47)
- Add comprehensive API-key authentication system with X-API-Key header validation
- Implement permission-based access control (mail:send, * for admin)
- Add rate-limiting system (60 req/hour per API key, 100 req/hour per IP)
- Refactor monolithic 590-line main.lua into 6 modular components (<200 lines each)
- Add IP-restriction support with CIDR notation (127.0.0.1, 10.0.0.0/8)
- Implement Hugo integration with CORS support for localhost:1313
- Add production-ready configuration with environment variable support
- Create comprehensive testing suite (auth, rate-limiting, stress tests)
- Add production deployment checklist and cleanup scripts

This refactoring transforms the API gateway from a single-file monolith into a
biocodie-compliant modular architecture while adding enterprise-grade security
features. Performance testing shows 79 RPS concurrent throughput with <100ms
latency. Hugo contact form integration tested and working. System is now
production-ready for deployment to walter/aitvaras.

Resolves #47
2025-06-24 22:01:38 +02:00
445e751c16 feat(api): implement CORS support with environment-based configuration
- Add CORS headers to all API responses in main.lua
- Implement OPTIONS preflight request handling
- Add environment-variable based CORS origin configuration
- Create production.env.example for deployment documentation
- Update .env.example with CORS_ALLOWED_ORIGINS setting

Resolves cross-origin request blocking for Hugo dev server integration.
CORS origins now configurable via CORS_ALLOWED_ORIGINS environment variable
for production deployments while maintaining dev-friendly defaults.

Related to #49
2025-06-24 19:42:44 +02:00
9ea0cb43e4 fix(deploy): prevent environment config overwrite and fix SMTP_HOST variable
- Fix create_environment_file() to preserve existing production config
- Change SMTP_SERVER to SMTP_HOST for consistency with other configs
- Add config existence check before creating new environment file
- Preserve permissions on existing config files
- Prevent accidental production config loss on redeployment

Fixes #50
2025-06-24 11:04:19 +02:00
0abc9791d3 fix(deployment): resolve OpenBSD rc.d service tracking and deployment workflow
- Fix OpenBSD service file pexp pattern to match actual running process
- Set pexp after sourcing rc.subr to prevent automatic override
- Update deployment script process detection from broken furt-lua pattern
- Add TTY-based daemon detection in start.sh for service vs development mode
- Implement comprehensive deployment workflow with backup and health checks
- Enable proper rcctl start/stop/check functionality on OpenBSD

Root cause: OpenBSD rc.subr automatically generates pexp from daemon+flags,
but actual process (/usr/local/bin/lua src/main.lua) differs from wrapper
(start.sh). Solution: Override pexp after rc.subr with correct Lua pattern.

Deployment script also had incorrect process detection pattern looking for
'furt-lua' string that doesn't exist in process name.

Technical details:
- Service file: pexp="/usr/local/bin/lua src/main.lua.*" after rc.subr
- Process detection: pgrep -u _furt -f 'src/main.lua'
- TTY detection: [ ! -t 0 ] for daemon vs interactive mode
- Complete deployment workflow with stop/sync/start/health-check cycle

Fixes #77 - OpenBSD rc.d service file problem resolved
Related: Deployment automation now fully functional karl→walter
2025-06-23 19:44:21 +02:00
c22b3aa691 feat(deployment): implement comprehensive karl→walter deployment script
- Add automated rsync-based file synchronization with _furt user permissions
- Implement OpenBSD rcctl service management with backup/rollback functionality
- Add port availability checks and health validation after deployment
- Include comprehensive error handling and status reporting
- Support dry-run mode for safe deployment testing
- Provide automatic service file generation with correct paths

Features:
- SSH-based secure transfer with permission preservation
- Pre-deployment backup with configurable retention (3 backups)
- Intelligent service stop/start handling for OpenBSD rcctl
- Health check validation via HTTP endpoint
- Colored output and structured logging for better UX
- Support for --dry-run, --rollback, and --force modes

Successfully deploys furt-lua from development (karl) to staging (walter).
Manual service management required due to OpenBSD rc.d pexp pattern issues.

Closes #76 (deployment automation)
Related: Service file pexp pattern matching requires follow-up investigation

Files:
- scripts/deploy/deploy_walter.sh (new)
2025-06-23 11:57:42 +02:00
e23b24d5d0 feat(smtp): implement universal SSL compatibility for luaossl and luasec (#74)
- Add automatic SSL library detection (luaossl/luasec)
- Support Arch Linux (luaossl) and OpenBSD (luasec)
- Maintain backward compatibility with existing configurations
- Enable production deployment on OpenBSD with _furt service user
- Implement transparent API abstraction for different SSL libraries

Technical improvements:
- Auto-detection prevents manual SSL library configuration
- Compatible with package managers (no custom builds required)
- Tested on karl (Arch/luaossl) and walter (OpenBSD/luasec)
- Both systems successfully send emails via Port 465 SSL
- DKIM authentication passes on both platforms

Production readiness:
- Service user compatibility (_furt on OpenBSD)
- Config detection (/usr/local/etc/furt/environment)
- Multi-distribution support (Arch + OpenBSD)
- No OpenSSL command dependencies (tech sovereignty compliance)

Fixes #74

Files modified:
- furt-lua/src/smtp.lua
2025-06-23 08:27:59 +02:00
010371a9a7 fix(config): resolve config loading path conflicts in universal start.sh (#68, #70)
- Split REPO_ROOT and PROJECT_DIR for different purposes
- REPO_ROOT: Repository-wide configs (.env, system configs)
- PROJECT_DIR: Lua-specific working directory (src/, cd)
- Fix config detection across development and production environments

Changes:
- REPO_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" # 2 levels up for .env
- PROJECT_DIR="$(dirname "$SCRIPT_DIR")"            # 1 level up for src/
- Config loading uses REPO_ROOT (.env location)
- Working directory and Lua paths use PROJECT_DIR (furt-lua/)

Tested on:
- karl (Linux/Development): .env loading + lua51 execution 
- walter (OpenBSD/Production): system config + lua execution 

Cross-platform SMTP functionality verified:
- karl: Full E2E test with successful mail delivery
- walter: HTTP server + config detection working

Fixes #68 (Universal Config Detection)
Fixes #70 (karl start.sh regression after universal script update)
2025-06-22 20:27:11 +02:00
bb2bed80a6 fix(config): partial implementation of universal config detection (#68)
- Add multi-distribution config path detection
- Support /usr/local/etc/furt/environment (OpenBSD)
- Support /etc/furt/environment (Debian/Ubuntu/RHEL)
- Maintain .env support for development
- Add configurable Lua command via LUA_COMMAND environment variable

POSIX compatibility improvements:
- Replace command -v with [ -x ] for service user compatibility
- Change shebang from #!/bin/bash to #!/bin/sh for OpenBSD
- Replace ${BASH_SOURCE[0]} with $0 for POSIX shell compatibility

walter deployment changes:
- Migrate directory structure to /usr/local/furt/furt-lua/
- Set up _furt user permissions
- Configure system config in /usr/local/etc/furt/environment

Known issues:
- karl development environment regression (lua51 detection failed)
- walter SSL missing for SMTP (luasec vs luaossl compatibility)
- Config strategy needs comprehensive redesign for multi-implementation

Related: #68

Files modified:
- furt-lua/scripts/start.sh (POSIX compatibility + universal config)
- .env.example (added LUA_COMMAND and LUA_VERSION)
- walter: /usr/local/etc/furt/environment (system config setup)
2025-06-22 18:39:38 +02:00
6d7d8a2af8 feat(smtp): complete native Lua SMTP integration for production mail delivery
- Add native Lua SMTP client with SSL/TLS support for mail.dragons-at-work.de:465
- Implement POST /v1/mail/send endpoint with real email delivery functionality
- Add environment variable integration (SMTP_*) for secure credential management
- Add comprehensive input validation and error handling for mail requests
- Add health check endpoint with SMTP configuration status reporting
- Add multi-line SMTP response handling for robust server communication
- Add request ID tracking system for debugging and monitoring
- Update start.sh script for automatic .env loading and dependency checking
- Add complete testing suite for SMTP functionality verification

This completes the Week 2 Challenge migration from Go to pure Lua HTTP server
with full production-ready SMTP capabilities. The implementation eliminates all
Google/corporate dependencies while achieving superior performance (18ms response
time) and maintaining digital sovereignty principles.

Real mail delivery confirmed: test email successfully sent to admin@dragons-at-work.de
Ready for Hugo website integration and production deployment with security layer.

Closes #65
2025-06-19 09:52:15 +02:00
662bfc7b7a feat(furt): implement complete Lua HTTP-Server for digital sovereignty (#63)
- Add furt-lua/ directory with pure Lua implementation
- Replace planned Go implementation with Corporate-free technology
- Complete Week 1 Challenge: HTTP-Server to production-ready in 48min

- HTTP-Server in pure Lua (185 lines, lua-socket based)
- JSON API endpoints with request/response parsing
- Modular architecture: each file < 200 lines
- Error handling for 404, 400, validation scenarios

- GET /health - Service health check with timestamp
- POST /test - Development testing with request echo
- POST /v1/mail/send - Mail service foundation with validation
- Comprehensive error responses with structured JSON

- Smart startup script with dependency auto-detection
- Automated test suite with lua-socket HTTP client
- Manual curl test suite for development workflow
- Complete documentation and installation guide

- FROM: Go (Google-controlled) → TO: Lua (PUC-Rio University)
- Corporate-free dependency chain: lua-socket + lua-cjson + lua-ssl
- Performance superior: < 1ms response time, minimal memory usage
- Foundation for planned C+Lua hybrid architecture

- furt-lua/src/main.lua - HTTP-Server implementation
- furt-lua/config/server.lua - Lua-based configuration
- furt-lua/scripts/start.sh - Startup with dependency checks
- furt-lua/scripts/test_curl.sh - Manual testing suite
- furt-lua/tests/test_http.lua - Automated test framework
- furt-lua/README.md - Implementation documentation

- README.md - Document Go→Lua migration strategy
- .gitignore - Add Lua artifacts, luarocks, issue-scripts

All endpoints tested and working:
✓ Health check returns proper JSON status
✓ Test endpoint processes POST requests with JSON
✓ Mail endpoint validates required fields (name, email, message)
✓ Error handling returns appropriate HTTP status codes

Ready for Week 2: SMTP integration with mail.dragons-at-work.de

Completes #63
Related #62
2025-06-17 20:40:40 +02:00
10b795ce13 refactor(architecture): migrate Furt concept from Go to C+Lua for digital sovereignty
- Replace Go-based architecture with C+Lua hybrid approach
- Eliminate Google-controlled dependencies per tech-reference analysis
- Add master strategy document for 18-24 month migration roadmap
- Define modular architecture with <200 lines per script constraint
- Specify Pure Lua → C+Lua → OpenBSD migration path
- Document SMTP integration for mail.dragons-at-work.de

Files modified:
- devdocs/KONZEPT.md (complete rewrite)
- devdocs/MASTER_STRATEGY.md (new)

Resolves concept phase, enables Week 1 Lua implementation.
2025-06-17 19:30:34 +02:00
78b70cf06b archive: move all generation-1 scripts to archive
- create_issue.sh: 800+ lines monster (functional but unmaintainable)
- update_script_labels.sh: 15KB auto-update system (too complex)
- get_issues.sh: 7KB grown over time (needs simplification)
- update_issue.sh: 5KB functional (part of old system)

Preparing clean slate for low-tech generation-2 system
2025-06-04 18:29:48 +02:00
53 changed files with 4221 additions and 4365 deletions

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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"

49
.gitignore vendored
View file

@ -1,26 +1,32 @@
# Environment variables (NEVER commit!) # Environment variables (NEVER commit!)
.env .env
.env.local
.env.production
.env.development
# Note: .env.example SHOULD be included for users
# Go build artifacts # Lua specific
*.exe *.luac
*.exe~ .luarocks/
*.dll luarocks.lock
*.so
*.dylib
furt-gateway
formular2mail-service
sagjan-service
/build/
/dist/
# Go test files # Furt runtime/build artifacts
*.test bin/
*.out logs/
coverage.txt tmp/
coverage.html pid/
dist/
scripts/upload-package.sh
# Go modules # Issue creation scripts (these create issues, don't version them)
/vendor/ scripts/gitea-issues/
# Gitea internal workflow (not for end users)
.gitea/
# Gitea Tools
tools/gitea
issue-*.md
# OS generated files # OS generated files
.DS_Store .DS_Store
@ -57,6 +63,9 @@ debug.log
*.sqlite3 *.sqlite3
# Configuration files with secrets # Configuration files with secrets
config.local.yaml config.local.lua
config.production.yaml config.production.lua
config/furt.conf
scripts/production_test_sequence.sh

33
.version_history Normal file
View file

@ -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

21
LICENSE
View file

@ -1,7 +1,18 @@
Apache License ISC license
Version 2.0, January 2004
http://www.apache.org/licenses/
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]

View file

@ -1,37 +1,83 @@
# Furt API Gateway # 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 ## Ü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 - HTTP-Server mit JSON-APIs
- **Digitale Souveränität**: Vollständige Kontrolle über die eigene Infrastruktur - Multi-Tenant Mail-Routing über SMTP
- **Native Deployment**: Go-Binaries ohne externe Abhängigkeiten - API-Key-basierte Authentifizierung
- **Ressourcenschonend**: Minimaler Speicher- und CPU-Verbrauch - Health-Check-Endpoints
- **Open Source**: Transparent und gemeinschaftlich entwickelt - 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 # Arch Linux
- **sagjan**: Selbst-gehostetes Kommentarsystem sudo pacman -S lua51 lua51-socket lua51-dkjson lua51-sec
- **Weitere**: Shop, Newsletter, Terminbuchung, etc. ```
## 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.

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.1.4

102
config/furt.conf.example Normal file
View file

@ -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

133
config/server.lua Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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
<VirtualHost *:443>
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
</VirtualHost>
```
### 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.

View file

@ -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<Komponente><Funktionsname><Szenario>`
- Beispiel: `TestGatewayRoutingWithValidAPIKey`, `TestServiceProxyWhenServiceUnavailable`
- Benchmark-Tests: `Benchmark<Funktionsname>`
- Example-Tests: `Example<Funktionsname>`
### 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.

View file

@ -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.*

View file

@ -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:** _______________

View file

@ -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 "$@"

3
go.mod
View file

@ -1,3 +0,0 @@
module furt
go 1.24.3

122
install.sh Executable file
View file

@ -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"

352
integrations/lua-api.lua Normal file
View file

@ -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

168
scripts/build-package.sh Executable file
View file

@ -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 "$@"

61
scripts/cleanup_debug.sh Executable file
View file

@ -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"

41
scripts/create-service.sh Executable file
View file

@ -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

View file

@ -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

View file

@ -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

40
scripts/health-check.sh Executable file
View file

@ -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

18
scripts/manual_mail_test.sh Executable file
View file

@ -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"

32
scripts/setup-directories.sh Executable file
View file

@ -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)"

20
scripts/setup-user.sh Executable file
View file

@ -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"

126
scripts/start.sh Executable file
View file

@ -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

171
scripts/stress_test.sh Executable file
View file

@ -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"

39
scripts/sync-files.sh Executable file
View file

@ -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"

79
scripts/test_auth.sh Executable file
View file

@ -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"

94
scripts/test_curl.sh Executable file
View file

@ -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}"

61
scripts/test_modular.sh Executable file
View file

@ -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"

132
scripts/test_smtp.sh Executable file
View file

@ -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"

View file

@ -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

View file

@ -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 <name> [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 <name> 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

50
scripts/validate-config.sh Executable file
View file

@ -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"

139
src/auth.lua Normal file
View file

@ -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

237
src/config_parser.lua Normal file
View file

@ -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

255
src/http_server.lua Normal file
View file

@ -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

117
src/ip_utils.lua Normal file
View file

@ -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

34
src/main.lua Normal file
View file

@ -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()

153
src/rate_limiter.lua Normal file
View file

@ -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

16
src/routes/auth.lua Normal file
View file

@ -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

80
src/routes/health.lua Normal file
View file

@ -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

169
src/routes/mail.lua Normal file
View file

@ -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

349
src/smtp.lua Normal file
View file

@ -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

273
tests/test_http.lua Normal file
View file

@ -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
}