diff --git a/composer.json b/composer.json index 048a573..3ac3600 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "dashboard/unifi", "description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform", - "version": "1.6.1", + "version": "1.6.2", "type": "library", "license": "MIT", "autoload": { diff --git a/src/Console/RotatePasswords.php b/src/Console/RotatePasswords.php index f552f37..f4a1425 100644 --- a/src/Console/RotatePasswords.php +++ b/src/Console/RotatePasswords.php @@ -68,6 +68,9 @@ class RotatePasswords extends Command if ($rotated) { Setting::set('unifi.password_rotation.last_rotated_at', now()->toIso8601String()); + // Persist the active password so it can be displayed in + // the Settings page and exposed via the API endpoint. + Setting::set('unifi.password_rotation.last_password', $password); $this->info('Rotated password for ' . count($rotated) . ' SSID(s).'); } diff --git a/src/Http/Controllers/UnifiSettingsController.php b/src/Http/Controllers/UnifiSettingsController.php index a24f281..a061cb5 100644 --- a/src/Http/Controllers/UnifiSettingsController.php +++ b/src/Http/Controllers/UnifiSettingsController.php @@ -31,10 +31,25 @@ class UnifiSettingsController extends Controller 'rotationMinute' => (int) Setting::get('unifi.password_rotation.minute', 0), 'rotationWordlist' => Setting::get('unifi.password_rotation.wordlist', ''), 'rotationLastRotatedAt' => Setting::get('unifi.password_rotation.last_rotated_at', null), + 'rotationLastPassword' => Setting::get('unifi.password_rotation.last_password', null), 'ppskSchedulingEnabled' => (bool) Setting::get('unifi.ppsk_scheduling.enabled', false), + 'apiToken' => Setting::get('unifi.api_token', null), ]); } + public function regenerateApiToken() + { + $token = bin2hex(random_bytes(24)); + Setting::set('unifi.api_token', $token); + return response()->json(['token' => $token]); + } + + public function clearApiToken() + { + Setting::set('unifi.api_token', ''); + return response()->json(['ok' => true]); + } + public function update(Request $request) { $request->validate([ diff --git a/src/Http/Controllers/WifiApiController.php b/src/Http/Controllers/WifiApiController.php new file mode 100644 index 0000000..30d35e3 --- /dev/null +++ b/src/Http/Controllers/WifiApiController.php @@ -0,0 +1,40 @@ +json(['error' => 'API token not configured'], 503); + } + + $provided = $request->bearerToken() ?: $request->query('token'); + if (! $provided || ! hash_equals($expected, $provided)) { + return response()->json(['error' => 'Unauthorized'], 401); + } + + $password = Setting::get('unifi.password_rotation.last_password'); + if (! $password) { + return response()->json([ + 'error' => 'No rotated password recorded yet — wait for the next scheduled rotation or run unifi:rotate-passwords --force.', + ], 404); + } + + return response()->json([ + 'password' => $password, + 'rotated_at' => Setting::get('unifi.password_rotation.last_rotated_at'), + ]); + } +} diff --git a/src/routes/unifi.php b/src/routes/unifi.php index 5234127..2419439 100644 --- a/src/routes/unifi.php +++ b/src/routes/unifi.php @@ -9,6 +9,7 @@ use Dashboard\Unifi\Http\Controllers\UnifiPagesAccessController; use Dashboard\Unifi\Http\Controllers\UnifiSettingsController; use Dashboard\Unifi\Http\Controllers\VlanGroupController; use Dashboard\Unifi\Http\Controllers\WebhookController; +use Dashboard\Unifi\Http\Controllers\WifiApiController; use Dashboard\Unifi\Http\Controllers\WifiController; use Illuminate\Support\Facades\Route; @@ -90,9 +91,20 @@ Route::middleware(['web', 'auth', 'app.access:unifi']) Route::delete('/settings/webhooks/{webhook}', [WebhookController::class, 'destroy'])->name('webhooks.destroy'); Route::post('/settings/webhooks/{webhook}/test', [WebhookController::class, 'test']) ->name('webhooks.test'); Route::post('/settings/webhooks/test-url', [WebhookController::class, 'testUrl'])->name('webhooks.test-url'); + + // API-token management + Route::post('/settings/api-token/regenerate', [UnifiSettingsController::class, 'regenerateApiToken'])->name('settings.api-token.regenerate'); + Route::delete('/settings/api-token', [UnifiSettingsController::class, 'clearApiToken']) ->name('settings.api-token.clear'); }); }); +// ── Public API (token-protected) ────────────────────────────────────────── +// External integrations (signage, kiosks) hit these without session auth. +Route::prefix('api/unifi')->name('unifi.api.')->group(function () { + Route::get('/wifi/current-password', [WifiApiController::class, 'currentPassword']) + ->name('wifi.current-password'); +}); + // ── Captive portal callback (public — user redirected here by UniFi) ───── Route::middleware(['web', 'auth']) ->get('/portal/wifi/callback', [PortalController::class, 'captiveCallback'])