feat: v1.1.0 - Docker, i18n, setup wizard, rate limiting, OpenAPI

New features:
- Docker support (Dockerfile + docker-compose.yml)
- GitHub Actions CI pipeline (PHP lint, JS lint, Docker build, i18n validation)
- Internationalization system with 3 languages (it, en, de) and 347 translation keys
- First-run setup wizard (4-step configuration)
- File-based API rate limiting (120/15/5 req/min tiers)
- OpenAPI 3.1.0 specification for all 43 API endpoints
- CONTRIBUTING.md with translation and development guide
- Screenshots directory placeholder

Modified:
- README.md: Docker badges, install instructions, translations section
- api/index.php: rate limiting middleware
- assets/js/app.js: i18n system, setup wizard, t() function
- assets/css/style.css: setup wizard styles
- index.html: data-i18n attributes, setup wizard overlay, language settings
- .gitignore: rate_limits exclusion
This commit is contained in:
dadaloop82
2026-04-10 06:03:11 +00:00
parent e0956c6043
commit d13f744aea
16 changed files with 2993 additions and 102 deletions
+68
View File
@@ -52,6 +52,74 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
// ===== RATE LIMITING =====
/**
* Simple file-based rate limiter.
* Limits: 120 req/min general, 15 req/min for AI endpoints, 5 req/min for login.
*/
function checkRateLimit(string $action): void {
$rateLimitDir = __DIR__ . '/../data/rate_limits';
if (!is_dir($rateLimitDir)) {
mkdir($rateLimitDir, 0755, true);
}
// Determine limit based on action
$aiActions = ['gemini_readExpiry', 'gemini_chat', 'gemini_identify', 'gemini_suggest_shopping'];
$loginActions = ['dupliclick_login'];
if (in_array($action, $aiActions)) {
$limit = 15;
$window = 60;
$bucket = 'ai';
} elseif (in_array($action, $loginActions)) {
$limit = 5;
$window = 60;
$bucket = 'login';
} else {
$limit = 120;
$window = 60;
$bucket = 'general';
}
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
$file = $rateLimitDir . '/' . md5($ip . '_' . $bucket) . '.json';
// Clean up old rate limit files periodically (1% chance per request)
if (mt_rand(1, 100) === 1) {
foreach (glob($rateLimitDir . '/*.json') as $f) {
if (filemtime($f) < time() - 300) @unlink($f);
}
}
$now = time();
$data = [];
if (file_exists($file)) {
$raw = @file_get_contents($file);
if ($raw) $data = json_decode($raw, true) ?: [];
}
// Remove entries outside the window
$data = array_values(array_filter($data, function($ts) use ($now, $window) {
return $ts > $now - $window;
}));
if (count($data) >= $limit) {
http_response_code(429);
header('Retry-After: ' . $window);
echo json_encode(['error' => 'Too many requests. Please try again later.']);
exit;
}
$data[] = $now;
@file_put_contents($file, json_encode($data), LOCK_EX);
}
// Apply rate limiting
$rateLimitAction = $_GET['action'] ?? '';
if ($rateLimitAction) {
checkRateLimit($rateLimitAction);
}
try {
$db = getDB();
} catch (Exception $e) {