feat: rate limiting + internal health monitor + secure VAPID note

Rate limiting:
- Shop POST endpoints get per-IP throttles with distinct prefixes so login,
  register, password-email, and password-reset have separate buckets:
  login/register/pw-reset = 5/min, pw-email = 3/min
- OcrInvoiceService gates per-tenant via RateLimiter (30/hour) so a runaway
  uploader can't burn Claude Vision spend

Health monitor (poor-man's monitoring):
- HealthCheckCommand probes DB (SELECT 1), cache write/read, public storage
  write/read, and most-recent backup age. On any failure, pushes a Telegram
  alert via HEALTH_ALERT_BOT_TOKEN/HEALTH_ALERT_CHAT_ID. Dedups identical
  failures within a 30-min window via cache.
- Scheduled every 10 min. Pair with external uptime monitoring (UptimeRobot,
  Better Stack hitting /up) for total-outage coverage.
- .env.example documents the two new env vars.

VAPID secret hygiene:
- credentials.md no longer stores the VAPID_PRIVATE_KEY; the source of truth
  is the Coolify env on the autocrm app. Doc points to where to read it
  (UI or API). Mitigates accidental git leak.

Tests (4 new):
- shop login throttles after 5 attempts (6th = 429); register throttle is
  independent of login (separate prefix); health command runs clean; dedup
  cache path exercised

Full suite: 138 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 06:37:53 +00:00
parent 0e3f9e8bca
commit 51917bcbaf
6 changed files with 237 additions and 4 deletions
+8
View File
@@ -37,3 +37,11 @@ ScheduleFacade::command('tires:remind-seasonal')
->weeklyOn(1, '09:30')
->withoutOverlapping()
->onOneServer();
// Internal health probe every 10 min — pushes Telegram alerts via
// HEALTH_ALERT_BOT_TOKEN/CHAT_ID env when DB/cache/storage/backup fails.
// Pair with external uptime monitoring for total-outage coverage.
ScheduleFacade::command('health:check --silent')
->everyTenMinutes()
->withoutOverlapping()
->onOneServer();
+6 -4
View File
@@ -94,18 +94,20 @@ Route::controller(\App\Http\Controllers\ShopController::class)->prefix('shop')->
});
// ─── Shop customer auth ────────────────────────────────────────────
// Aggressive throttle on auth POSTs (per IP) to prevent brute force and
// credential stuffing; GET views and account stay unthrottled.
Route::controller(\App\Http\Controllers\ShopAuthController::class)->prefix('shop')->group(function () {
Route::get('/register', 'showRegister')->name('shop.register');
Route::post('/register', 'register');
Route::post('/register', 'register')->middleware('throttle:5,1,shop-register');
Route::get('/login', 'showLogin')->name('shop.login');
Route::post('/login', 'login');
Route::post('/login', 'login')->middleware('throttle:5,1,shop-login');
Route::post('/logout', 'logout')->name('shop.logout');
Route::get('/account', 'account')->name('shop.account');
Route::get('/password/forgot', 'showForgotPassword')->name('shop.password.forgot');
Route::post('/password/email', 'sendResetLink')->name('shop.password.email');
Route::post('/password/email', 'sendResetLink')->name('shop.password.email')->middleware('throttle:3,1,shop-pw-email');
Route::get('/password/reset/{token}', 'showResetPassword')->name('password.reset');
Route::post('/password/reset', 'resetPassword')->name('shop.password.update');
Route::post('/password/reset', 'resetPassword')->name('shop.password.update')->middleware('throttle:5,1,shop-pw-reset');
});
// ─── Public WO tracking (no auth, tenant-scoped via subdomain) ──────