Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a33f2885ff | |||
| a4397c5178 | |||
| fc4f5370ae | |||
| e59f193ffc | |||
| f7672771e0 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "dashboard/unifi",
|
"name": "dashboard/unifi",
|
||||||
"description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform",
|
"description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform",
|
||||||
"version": "1.0.0",
|
"version": "1.4.0",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Per-page access grants for unifi pages. A user can access a unifi
|
||||||
|
* page if ANY of these hold:
|
||||||
|
* - is_super_admin (always)
|
||||||
|
* - user has the page's required_permission (existing nav_items column)
|
||||||
|
* - user is in the page's required_group_id (existing column)
|
||||||
|
* - a row here grants them as a user, or via a group they're in
|
||||||
|
*
|
||||||
|
* Snap-in-local table — disappears with the snap-in if uninstalled.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('unifi_page_grants', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('nav_item_id')->constrained('nav_items')->cascadeOnDelete();
|
||||||
|
$table->enum('grantee_type', ['user', 'group']);
|
||||||
|
$table->unsignedBigInteger('grantee_id');
|
||||||
|
$table->foreignId('granted_by_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['nav_item_id', 'grantee_type', 'grantee_id'], 'unifi_page_grants_unique');
|
||||||
|
$table->index(['grantee_type', 'grantee_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('unifi_page_grants');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -81,7 +81,7 @@ class RotatePasswords extends Command
|
|||||||
$hour = (int) Setting::get('unifi.password_rotation.hour', 2);
|
$hour = (int) Setting::get('unifi.password_rotation.hour', 2);
|
||||||
$minute = (int) Setting::get('unifi.password_rotation.minute', 0);
|
$minute = (int) Setting::get('unifi.password_rotation.minute', 0);
|
||||||
$dow = (int) Setting::get('unifi.password_rotation.day_of_week', 0);
|
$dow = (int) Setting::get('unifi.password_rotation.day_of_week', 0);
|
||||||
$tz = Setting::get('unifi.timezone', 'UTC');
|
$tz = \App\Support\Timezone::current();
|
||||||
$now = now($tz);
|
$now = now($tz);
|
||||||
|
|
||||||
if ($now->hour !== $hour || $now->minute !== $minute) {
|
if ($now->hour !== $hour || $now->minute !== $minute) {
|
||||||
|
|||||||
@@ -14,16 +14,21 @@ class SyncPpskSchedules extends Command
|
|||||||
|
|
||||||
public function handle(UnifiApiClient $unifi): int
|
public function handle(UnifiApiClient $unifi): int
|
||||||
{
|
{
|
||||||
if (! $this->option('force') && ! Setting::get('unifi.ppsk_scheduling.enabled')) {
|
// Always run, even when global ppsk_scheduling is disabled — in
|
||||||
return self::SUCCESS;
|
// that case the target state for every PPSK is "active" (always
|
||||||
}
|
// on). That way disabling the global setting actually restores
|
||||||
|
// any held PPSKs to active without operators having to do
|
||||||
|
// anything else, and null-schedule PPSKs always end up active.
|
||||||
|
// Schedules in the DB are preserved regardless of toggle state,
|
||||||
|
// so re-enabling resumes the per-PPSK schedule.
|
||||||
|
$globalEnabled = (bool) Setting::get('unifi.ppsk_scheduling.enabled');
|
||||||
|
|
||||||
$tz = Setting::get('unifi.timezone', 'UTC');
|
$tz = \App\Support\Timezone::current();
|
||||||
$now = now($tz);
|
$now = now($tz);
|
||||||
$day = $now->dayOfWeek; // 0=Sun … 6=Sat
|
$day = $now->dayOfWeek; // 0=Sun … 6=Sat
|
||||||
$slot = $now->hour * 2 + ($now->minute >= 30 ? 1 : 0); // 0–47
|
$slot = $now->hour * 2 + ($now->minute >= 30 ? 1 : 0); // 0–47
|
||||||
|
|
||||||
$ppsks = UnifiPpsk::whereNotNull('schedule')->get();
|
$ppsks = UnifiPpsk::all();
|
||||||
|
|
||||||
if ($ppsks->isEmpty()) {
|
if ($ppsks->isEmpty()) {
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
@@ -42,7 +47,12 @@ class SyncPpskSchedules extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($ppsks as $ppsk) {
|
foreach ($ppsks as $ppsk) {
|
||||||
$shouldBeOn = (bool) ($ppsk->schedule[$day * 48 + $slot] ?? true);
|
// Default to "always on". Only consult the schedule if
|
||||||
|
// global scheduling is enabled AND this PPSK has one.
|
||||||
|
$shouldBeOn = true;
|
||||||
|
if ($globalEnabled && $ppsk->schedule) {
|
||||||
|
$shouldBeOn = (bool) ($ppsk->schedule[$day * 48 + $slot] ?? true);
|
||||||
|
}
|
||||||
|
|
||||||
if ($shouldBeOn && $ppsk->state === 'held') {
|
if ($shouldBeOn && $ppsk->state === 'held') {
|
||||||
$this->enablePpsk($ppsk, $unifi, $networksByVlan);
|
$this->enablePpsk($ppsk, $unifi, $networksByVlan);
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Dashboard\Unifi\Http\Controllers;
|
|
||||||
|
|
||||||
use Dashboard\Unifi\Services\UnifiApiClient;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Routing\Controller;
|
|
||||||
use Inertia\Inertia;
|
|
||||||
|
|
||||||
class ApGroupController extends Controller
|
|
||||||
{
|
|
||||||
public function index(UnifiApiClient $unifi)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$groups = collect($unifi->getApGroups())->map(fn ($g) => [
|
|
||||||
'id' => $g['_id'],
|
|
||||||
'name' => $g['name'] ?? 'Unnamed',
|
|
||||||
'device_macs' => $g['device_macs'] ?? [],
|
|
||||||
'is_default' => $g['attr_no_delete'] ?? false,
|
|
||||||
])->values();
|
|
||||||
|
|
||||||
$devices = collect($unifi->getAccessPoints())->map(fn ($d) => [
|
|
||||||
'mac' => strtolower($d['mac']),
|
|
||||||
'name' => $d['name'] ?? $d['model'] ?? $d['mac'],
|
|
||||||
'model' => $d['model'] ?? '',
|
|
||||||
'state' => $d['state'] ?? 0,
|
|
||||||
])->values();
|
|
||||||
|
|
||||||
return Inertia::render('Unifi/ApGroups', [
|
|
||||||
'groups' => $groups,
|
|
||||||
'devices' => $devices,
|
|
||||||
]);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return Inertia::render('Unifi/ApGroups', [
|
|
||||||
'groups' => [], 'devices' => [], 'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(Request $request, UnifiApiClient $unifi)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'name' => 'required|string|max:100',
|
|
||||||
'device_macs' => 'present|array',
|
|
||||||
'device_macs.*' => 'string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = $unifi->createApGroup([
|
|
||||||
'name' => $data['name'],
|
|
||||||
'device_macs' => array_values(array_map('strtolower', $data['device_macs'])),
|
|
||||||
]);
|
|
||||||
return back()->with('success', 'AP group created.');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return back()->withErrors(['error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request, string $groupId, UnifiApiClient $unifi)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'name' => 'sometimes|string|max:100',
|
|
||||||
'device_macs' => 'sometimes|array',
|
|
||||||
'device_macs.*' => 'string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (isset($data['device_macs'])) {
|
|
||||||
$data['device_macs'] = array_values(array_map('strtolower', $data['device_macs']));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$unifi->updateApGroup($groupId, $data);
|
|
||||||
return back()->with('success', 'AP group updated.');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return back()->withErrors(['error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(string $groupId, UnifiApiClient $unifi)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$unifi->deleteApGroup($groupId);
|
|
||||||
return back()->with('success', 'AP group deleted.');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return back()->withErrors(['error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
82
src/Http/Controllers/UnifiPagesAccessController.php
Normal file
82
src/Http/Controllers/UnifiPagesAccessController.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Dashboard\Unifi\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\DashboardApp;
|
||||||
|
use App\Models\Group;
|
||||||
|
use App\Models\NavItem;
|
||||||
|
use App\Models\User;
|
||||||
|
use Dashboard\Unifi\Models\UnifiPageGrant;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Controller;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Super-admin-only endpoints for managing per-page access on unifi
|
||||||
|
* pages. Pages here = nav_items where app_id = unifi's DashboardApp row.
|
||||||
|
*/
|
||||||
|
class UnifiPagesAccessController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$app = DashboardApp::where('slug', 'unifi')->first();
|
||||||
|
if (! $app) {
|
||||||
|
return response()->json(['pages' => [], 'users' => [], 'groups' => []]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pages = NavItem::where('app_id', $app->id)
|
||||||
|
->where('is_folder', false)
|
||||||
|
->whereNotNull('route_name')
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get(['id', 'label', 'route_name']);
|
||||||
|
|
||||||
|
$grants = UnifiPageGrant::whereIn('nav_item_id', $pages->pluck('id'))
|
||||||
|
->get()
|
||||||
|
->groupBy('nav_item_id');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'pages' => $pages->map(fn ($p) => [
|
||||||
|
'id' => $p->id,
|
||||||
|
'label' => $p->label,
|
||||||
|
'route_name' => $p->route_name,
|
||||||
|
'user_ids' => $grants->get($p->id, collect())->where('grantee_type', 'user')->pluck('grantee_id')->all(),
|
||||||
|
'group_ids' => $grants->get($p->id, collect())->where('grantee_type', 'group')->pluck('grantee_id')->all(),
|
||||||
|
])->values(),
|
||||||
|
'users' => User::orderBy('name')->get(['id', 'name', 'email']),
|
||||||
|
'groups' => Group::orderBy('name')->get(['id', 'name', 'is_super']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, NavItem $navItem)
|
||||||
|
{
|
||||||
|
$app = DashboardApp::where('slug', 'unifi')->first();
|
||||||
|
if (! $app || $navItem->app_id !== $app->id) {
|
||||||
|
return response()->json(['error' => 'Not a unifi page.'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'user_ids' => 'present|array',
|
||||||
|
'user_ids.*' => 'integer|exists:users,id',
|
||||||
|
'group_ids' => 'present|array',
|
||||||
|
'group_ids.*' => 'integer|exists:groups,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$grantedBy = $request->user()?->id;
|
||||||
|
|
||||||
|
DB::transaction(function () use ($navItem, $data, $grantedBy) {
|
||||||
|
UnifiPageGrant::where('nav_item_id', $navItem->id)->delete();
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$now = now();
|
||||||
|
foreach ($data['user_ids'] as $uid) {
|
||||||
|
$rows[] = ['nav_item_id' => $navItem->id, 'grantee_type' => 'user', 'grantee_id' => $uid, 'granted_by_user_id' => $grantedBy, 'created_at' => $now, 'updated_at' => $now];
|
||||||
|
}
|
||||||
|
foreach ($data['group_ids'] as $gid) {
|
||||||
|
$rows[] = ['nav_item_id' => $navItem->id, 'grantee_type' => 'group', 'grantee_id' => $gid, 'granted_by_user_id' => $grantedBy, 'created_at' => $now, 'updated_at' => $now];
|
||||||
|
}
|
||||||
|
if ($rows) UnifiPageGrant::insert($rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json(['ok' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@ class UnifiSettingsController extends Controller
|
|||||||
'pollInterval' => (int) Setting::get('unifi.poll_interval', 30),
|
'pollInterval' => (int) Setting::get('unifi.poll_interval', 30),
|
||||||
'cacheTtl' => (int) Setting::get('unifi.cache_ttl', 30),
|
'cacheTtl' => (int) Setting::get('unifi.cache_ttl', 30),
|
||||||
'retentionDays' => (int) Setting::get('unifi.retention_days', 30),
|
'retentionDays' => (int) Setting::get('unifi.retention_days', 30),
|
||||||
'timezone' => Setting::get('unifi.timezone', 'UTC'),
|
|
||||||
'autoRebootEnabled' => (bool) Setting::get('unifi.auto_reboot.enabled', false),
|
'autoRebootEnabled' => (bool) Setting::get('unifi.auto_reboot.enabled', false),
|
||||||
'autoRebootFrequency' => Setting::get('unifi.auto_reboot.frequency', 'daily'),
|
'autoRebootFrequency' => Setting::get('unifi.auto_reboot.frequency', 'daily'),
|
||||||
'autoRebootDow' => (int) Setting::get('unifi.auto_reboot.day_of_week', 0),
|
'autoRebootDow' => (int) Setting::get('unifi.auto_reboot.day_of_week', 0),
|
||||||
@@ -45,7 +44,6 @@ class UnifiSettingsController extends Controller
|
|||||||
'poll_interval' => 'nullable|integer|min:5|max:300',
|
'poll_interval' => 'nullable|integer|min:5|max:300',
|
||||||
'cache_ttl' => 'nullable|integer|min:5|max:300',
|
'cache_ttl' => 'nullable|integer|min:5|max:300',
|
||||||
'retention_days' => 'nullable|integer|min:1|max:365',
|
'retention_days' => 'nullable|integer|min:1|max:365',
|
||||||
'timezone' => 'nullable|string|timezone',
|
|
||||||
'auto_reboot_enabled' => 'boolean',
|
'auto_reboot_enabled' => 'boolean',
|
||||||
'auto_reboot_frequency' => 'in:daily,weekly',
|
'auto_reboot_frequency' => 'in:daily,weekly',
|
||||||
'auto_reboot_dow' => 'nullable|integer|min:0|max:6',
|
'auto_reboot_dow' => 'nullable|integer|min:0|max:6',
|
||||||
@@ -70,7 +68,6 @@ class UnifiSettingsController extends Controller
|
|||||||
if ($request->has('poll_interval')) Setting::set('unifi.poll_interval', $request->poll_interval ?? 30);
|
if ($request->has('poll_interval')) Setting::set('unifi.poll_interval', $request->poll_interval ?? 30);
|
||||||
if ($request->has('cache_ttl')) Setting::set('unifi.cache_ttl', $request->cache_ttl ?? 30);
|
if ($request->has('cache_ttl')) Setting::set('unifi.cache_ttl', $request->cache_ttl ?? 30);
|
||||||
if ($request->has('retention_days')) Setting::set('unifi.retention_days', $request->retention_days ?? 30);
|
if ($request->has('retention_days')) Setting::set('unifi.retention_days', $request->retention_days ?? 30);
|
||||||
if ($request->has('timezone')) Setting::set('unifi.timezone', $request->timezone ?? 'UTC');
|
|
||||||
|
|
||||||
Setting::set('unifi.auto_reboot.enabled', $request->boolean('auto_reboot_enabled') ? '1' : '');
|
Setting::set('unifi.auto_reboot.enabled', $request->boolean('auto_reboot_enabled') ? '1' : '');
|
||||||
Setting::set('unifi.auto_reboot.frequency', $request->input('auto_reboot_frequency', 'daily'));
|
Setting::set('unifi.auto_reboot.frequency', $request->input('auto_reboot_frequency', 'daily'));
|
||||||
|
|||||||
@@ -16,17 +16,6 @@ class WifiController extends Controller
|
|||||||
try {
|
try {
|
||||||
$wlans = collect($unifi->getWlans())->map(fn ($w) => $this->mapWlan($w))->values();
|
$wlans = collect($unifi->getWlans())->map(fn ($w) => $this->mapWlan($w))->values();
|
||||||
|
|
||||||
try {
|
|
||||||
$apGroups = collect($unifi->getApGroups())->map(fn ($g) => [
|
|
||||||
'id' => $g['_id'],
|
|
||||||
'name' => $g['attr_no_delete'] ?? false ? 'Default' : ($g['name'] ?? 'Unnamed'),
|
|
||||||
'device_macs' => $g['device_macs'] ?? [],
|
|
||||||
'is_default' => $g['attr_no_delete'] ?? false,
|
|
||||||
])->values();
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$apGroups = collect(); // AP groups not supported by this controller
|
|
||||||
}
|
|
||||||
|
|
||||||
$raw = Setting::get('unifi.ssid_groups', '{}');
|
$raw = Setting::get('unifi.ssid_groups', '{}');
|
||||||
$groups = json_decode($raw, true);
|
$groups = json_decode($raw, true);
|
||||||
if (! is_array($groups) || array_is_list($groups)) $groups = [];
|
if (! is_array($groups) || array_is_list($groups)) $groups = [];
|
||||||
@@ -37,13 +26,12 @@ class WifiController extends Controller
|
|||||||
return Inertia::render('Unifi/Wifi', [
|
return Inertia::render('Unifi/Wifi', [
|
||||||
'wlans' => $wlans,
|
'wlans' => $wlans,
|
||||||
'groups' => $groups,
|
'groups' => $groups,
|
||||||
'apGroups' => $apGroups,
|
|
||||||
'rotateWlanIds' => $rotateWlanIds,
|
'rotateWlanIds' => $rotateWlanIds,
|
||||||
'ppskSchedulingEnabled' => (bool) Setting::get('unifi.ppsk_scheduling.enabled', false),
|
'ppskSchedulingEnabled' => (bool) Setting::get('unifi.ppsk_scheduling.enabled', false),
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return Inertia::render('Unifi/Wifi', [
|
return Inertia::render('Unifi/Wifi', [
|
||||||
'wlans' => [], 'groups' => [], 'apGroups' => [], 'rotateWlanIds' => [], 'error' => $e->getMessage(),
|
'wlans' => [], 'groups' => [], 'rotateWlanIds' => [], 'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,21 +86,6 @@ class WifiController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update AP group assignments for a single WLAN (not synced to group siblings).
|
|
||||||
*/
|
|
||||||
public function updateApGroups(Request $request, string $wlanId, UnifiApiClient $unifi)
|
|
||||||
{
|
|
||||||
$request->validate(['ap_group_ids' => 'required|array']);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$unifi->updateWlan($wlanId, ['ap_group_ids' => $request->ap_group_ids]);
|
|
||||||
return back()->with('success', 'AP groups updated.');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return back()->withErrors(['error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggle(Request $request, string $wlanId, UnifiApiClient $unifi)
|
public function toggle(Request $request, string $wlanId, UnifiApiClient $unifi)
|
||||||
{
|
{
|
||||||
$request->validate(['enabled' => 'required|boolean']);
|
$request->validate(['enabled' => 'required|boolean']);
|
||||||
@@ -414,7 +387,6 @@ class WifiController extends Controller
|
|||||||
'hide_ssid' => $w['hide_ssid'] ?? false,
|
'hide_ssid' => $w['hide_ssid'] ?? false,
|
||||||
'passphrase' => $w['x_passphrase'] ?? '',
|
'passphrase' => $w['x_passphrase'] ?? '',
|
||||||
'band' => $this->detectBand($w),
|
'band' => $this->detectBand($w),
|
||||||
'ap_group_ids' => $w['ap_group_ids'] ?? [],
|
|
||||||
'mac_filter_enabled' => $w['mac_filter_enabled'] ?? false,
|
'mac_filter_enabled' => $w['mac_filter_enabled'] ?? false,
|
||||||
'mac_filter_policy' => $w['mac_filter_policy'] ?? 'deny',
|
'mac_filter_policy' => $w['mac_filter_policy'] ?? 'deny',
|
||||||
'ppsk_enabled' => ($w['wpa3_ppsk'] ?? false)
|
'ppsk_enabled' => ($w['wpa3_ppsk'] ?? false)
|
||||||
|
|||||||
56
src/Models/UnifiPageGrant.php
Normal file
56
src/Models/UnifiPageGrant.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Dashboard\Unifi\Models;
|
||||||
|
|
||||||
|
use App\Models\NavItem;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class UnifiPageGrant extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'unifi_page_grants';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'nav_item_id',
|
||||||
|
'grantee_type',
|
||||||
|
'grantee_id',
|
||||||
|
'granted_by_user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function navItem(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(NavItem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function grantedBy(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'granted_by_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True iff $user is allowed to access $navItem under this grant model.
|
||||||
|
* Super-admins always pass.
|
||||||
|
* If there are NO grants for the page, falls back to "open" (anyone
|
||||||
|
* who can reach the route can access — same as before grants existed).
|
||||||
|
*/
|
||||||
|
public static function userCanAccess(User $user, NavItem $navItem): bool
|
||||||
|
{
|
||||||
|
if ($user->is_super_admin) return true;
|
||||||
|
|
||||||
|
$hasGrants = static::where('nav_item_id', $navItem->id)->exists();
|
||||||
|
if (! $hasGrants) return true;
|
||||||
|
|
||||||
|
$groupIds = $user->groups()->pluck('groups.id');
|
||||||
|
|
||||||
|
return static::where('nav_item_id', $navItem->id)
|
||||||
|
->where(function ($q) use ($user, $groupIds) {
|
||||||
|
$q->where(function ($u) use ($user) {
|
||||||
|
$u->where('grantee_type', 'user')->where('grantee_id', $user->id);
|
||||||
|
})->orWhere(function ($g) use ($groupIds) {
|
||||||
|
$g->where('grantee_type', 'group')->whereIn('grantee_id', $groupIds);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -312,28 +312,6 @@ class UnifiApiClient
|
|||||||
return $this->put("/rest/wlanconf/{$wlanId}", $data);
|
return $this->put("/rest/wlanconf/{$wlanId}", $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── AP Groups ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public function getApGroups(): array
|
|
||||||
{
|
|
||||||
return $this->get('/rest/apgroups');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createApGroup(array $data): array
|
|
||||||
{
|
|
||||||
return $this->post('/rest/apgroups', $data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateApGroup(string $groupId, array $data): array
|
|
||||||
{
|
|
||||||
return $this->put("/rest/apgroups/{$groupId}", $data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deleteApGroup(string $groupId): void
|
|
||||||
{
|
|
||||||
$this->delete("/rest/apgroups/{$groupId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── PPSK ─────────────────────────────────────────────────────────────────
|
// ── PPSK ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
namespace Dashboard\Unifi;
|
namespace Dashboard\Unifi;
|
||||||
|
|
||||||
|
use App\Models\DashboardApp;
|
||||||
|
use App\Models\NavItem;
|
||||||
|
use Dashboard\Unifi\Models\UnifiPageGrant;
|
||||||
|
use Illuminate\Routing\Events\RouteMatched;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class UnifiServiceProvider extends ServiceProvider
|
class UnifiServiceProvider extends ServiceProvider
|
||||||
@@ -20,6 +25,34 @@ class UnifiServiceProvider extends ServiceProvider
|
|||||||
$this->loadRoutesFrom(__DIR__ . '/routes/unifi.php');
|
$this->loadRoutesFrom(__DIR__ . '/routes/unifi.php');
|
||||||
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
||||||
|
|
||||||
|
// Per-page access enforcement for unifi routes. If a unifi page has
|
||||||
|
// any UnifiPageGrant rows, only super-admins and granted users/
|
||||||
|
// groups can hit it; otherwise (no grants) it's open per the existing
|
||||||
|
// permission middleware. Super-admins always bypass.
|
||||||
|
Event::listen(RouteMatched::class, function (RouteMatched $event) {
|
||||||
|
$routeName = $event->route->getName();
|
||||||
|
if (! $routeName || ! str_starts_with($routeName, 'unifi.')) return;
|
||||||
|
|
||||||
|
$user = $event->request->user();
|
||||||
|
if (! $user || $user->is_super_admin) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$appId = DashboardApp::where('slug', 'unifi')->value('id');
|
||||||
|
$item = NavItem::where('route_name', $routeName)
|
||||||
|
->where('app_id', $appId)
|
||||||
|
->first();
|
||||||
|
if (! $item) return;
|
||||||
|
|
||||||
|
if (! UnifiPageGrant::userCanAccess($user, $item)) {
|
||||||
|
abort(403, 'You do not have access to this page.');
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// unifi_page_grants table may not exist yet on a fresh
|
||||||
|
// install before this snap-in's migrations have run —
|
||||||
|
// fail open in that narrow window.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if ($this->app->runningInConsole()) {
|
if ($this->app->runningInConsole()) {
|
||||||
$this->commands([
|
$this->commands([
|
||||||
Console\CheckWebhooks::class,
|
Console\CheckWebhooks::class,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Dashboard\Unifi\Http\Controllers\ApGroupController;
|
|
||||||
use Dashboard\Unifi\Http\Controllers\ClientController;
|
use Dashboard\Unifi\Http\Controllers\ClientController;
|
||||||
use Dashboard\Unifi\Http\Controllers\DeviceController;
|
use Dashboard\Unifi\Http\Controllers\DeviceController;
|
||||||
use Dashboard\Unifi\Http\Controllers\PortalController;
|
use Dashboard\Unifi\Http\Controllers\PortalController;
|
||||||
use Dashboard\Unifi\Http\Controllers\StatsController;
|
use Dashboard\Unifi\Http\Controllers\StatsController;
|
||||||
|
use Dashboard\Unifi\Http\Controllers\UnifiPagesAccessController;
|
||||||
use Dashboard\Unifi\Http\Controllers\UnifiSettingsController;
|
use Dashboard\Unifi\Http\Controllers\UnifiSettingsController;
|
||||||
use Dashboard\Unifi\Http\Controllers\VlanGroupController;
|
use Dashboard\Unifi\Http\Controllers\VlanGroupController;
|
||||||
use Dashboard\Unifi\Http\Controllers\WebhookController;
|
use Dashboard\Unifi\Http\Controllers\WebhookController;
|
||||||
@@ -32,7 +32,6 @@ Route::middleware(['web', 'auth', 'app.access:unifi'])
|
|||||||
// WiFi networks
|
// WiFi networks
|
||||||
Route::get('/wifi', [WifiController::class, 'index']) ->name('wifi');
|
Route::get('/wifi', [WifiController::class, 'index']) ->name('wifi');
|
||||||
Route::put('/wifi/{wlanId}', [WifiController::class, 'update']) ->name('wifi.update');
|
Route::put('/wifi/{wlanId}', [WifiController::class, 'update']) ->name('wifi.update');
|
||||||
Route::put('/wifi/{wlanId}/ap-groups', [WifiController::class, 'updateApGroups']) ->name('wifi.ap-groups');
|
|
||||||
Route::post('/wifi/{wlanId}/toggle', [WifiController::class, 'toggle']) ->name('wifi.toggle');
|
Route::post('/wifi/{wlanId}/toggle', [WifiController::class, 'toggle']) ->name('wifi.toggle');
|
||||||
Route::post('/wifi/groups', [WifiController::class, 'saveGroups']) ->name('wifi.groups');
|
Route::post('/wifi/groups', [WifiController::class, 'saveGroups']) ->name('wifi.groups');
|
||||||
|
|
||||||
@@ -44,12 +43,6 @@ Route::middleware(['web', 'auth', 'app.access:unifi'])
|
|||||||
Route::put('/wifi/{wlanId}/ppsk/{ppskId}/schedule', [WifiController::class, 'ppskSchedule']) ->name('wifi.ppsk.schedule');
|
Route::put('/wifi/{wlanId}/ppsk/{ppskId}/schedule', [WifiController::class, 'ppskSchedule']) ->name('wifi.ppsk.schedule');
|
||||||
Route::patch('/wifi/{wlanId}/ppsk/{ppskId}/rotation',[WifiController::class, 'ppskToggleRotation'])->name('wifi.ppsk.rotation');
|
Route::patch('/wifi/{wlanId}/ppsk/{ppskId}/rotation',[WifiController::class, 'ppskToggleRotation'])->name('wifi.ppsk.rotation');
|
||||||
|
|
||||||
// AP Groups
|
|
||||||
Route::get('/ap-groups', [ApGroupController::class, 'index']) ->name('ap-groups.index');
|
|
||||||
Route::post('/ap-groups', [ApGroupController::class, 'store']) ->name('ap-groups.store');
|
|
||||||
Route::put('/ap-groups/{groupId}', [ApGroupController::class, 'update']) ->name('ap-groups.update');
|
|
||||||
Route::delete('/ap-groups/{groupId}', [ApGroupController::class, 'destroy']) ->name('ap-groups.destroy');
|
|
||||||
|
|
||||||
// Devices
|
// Devices
|
||||||
Route::post('/devices/reboot', [DeviceController::class, 'reboot']) ->name('devices.reboot');
|
Route::post('/devices/reboot', [DeviceController::class, 'reboot']) ->name('devices.reboot');
|
||||||
Route::post('/clients/kick', [ClientController::class, 'kick']) ->name('clients.kick');
|
Route::post('/clients/kick', [ClientController::class, 'kick']) ->name('clients.kick');
|
||||||
@@ -78,6 +71,13 @@ Route::middleware(['web', 'auth', 'app.access:unifi'])
|
|||||||
Route::post('/settings/test', [UnifiSettingsController::class, 'testConnection'])->name('settings.test');
|
Route::post('/settings/test', [UnifiSettingsController::class, 'testConnection'])->name('settings.test');
|
||||||
Route::post('/settings/sites', [UnifiSettingsController::class, 'fetchSites']) ->name('settings.sites');
|
Route::post('/settings/sites', [UnifiSettingsController::class, 'fetchSites']) ->name('settings.sites');
|
||||||
|
|
||||||
|
// Page Access — super-admin only. Lists unifi pages and lets
|
||||||
|
// operators assign per-page user/group grants.
|
||||||
|
Route::middleware('super.admin')->group(function () {
|
||||||
|
Route::get('/settings/pages-access', [UnifiPagesAccessController::class, 'index']) ->name('settings.pages-access.index');
|
||||||
|
Route::put('/settings/pages-access/{navItem}', [UnifiPagesAccessController::class, 'update']) ->name('settings.pages-access.update');
|
||||||
|
});
|
||||||
|
|
||||||
// Webhooks
|
// Webhooks
|
||||||
Route::get('/webhooks', [WebhookController::class, 'index']) ->name('webhooks.index');
|
Route::get('/webhooks', [WebhookController::class, 'index']) ->name('webhooks.index');
|
||||||
Route::post('/webhooks', [WebhookController::class, 'store']) ->name('webhooks.store');
|
Route::post('/webhooks', [WebhookController::class, 'store']) ->name('webhooks.store');
|
||||||
|
|||||||
Reference in New Issue
Block a user