Phase 5 (Admin Panel): - Audit log viewer: searchable with action/date filters, expandable rows showing JSON changes, color-coded action chips, user avatars - System settings: tabbed page (General, API Credentials, Billing, Notifications) with masked sensitive values, per-group save - Settings model with get/set/getGroup/setGroup helpers - Settings migration for key-value store with groups - 52 tests passing, build clean Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
138 lines
4.3 KiB
PHP
138 lines
4.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\Admin\UpdateSettingsRequest;
|
|
use App\Models\Setting;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class SettingsController extends Controller
|
|
{
|
|
/**
|
|
* The setting keys for each group, with their defaults.
|
|
*
|
|
* @var array<string, array<string, string|null>>
|
|
*/
|
|
private const SETTING_DEFAULTS = [
|
|
'general' => [
|
|
'company_name' => 'EZSCALE',
|
|
'company_email' => null,
|
|
'support_url' => null,
|
|
'status_page_url' => null,
|
|
],
|
|
'api' => [
|
|
'virtfusion_api_url' => null,
|
|
'virtfusion_api_token' => null,
|
|
'synergycp_api_url' => null,
|
|
'synergycp_api_token' => null,
|
|
'enhance_api_url' => null,
|
|
'enhance_api_token' => null,
|
|
],
|
|
'billing' => [
|
|
'default_currency' => 'USD',
|
|
'grace_period_days' => '7',
|
|
'suspension_warning_days' => '3',
|
|
'auto_terminate_days' => '14',
|
|
'bandwidth_overage_rate' => '0.05',
|
|
],
|
|
'notifications' => [
|
|
'discord_webhook_url' => null,
|
|
'slack_webhook_url' => null,
|
|
'email_from_address' => null,
|
|
'email_from_name' => null,
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Keys that contain sensitive values and should be masked in the UI.
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
private const SENSITIVE_KEYS = [
|
|
'virtfusion_api_token',
|
|
'synergycp_api_token',
|
|
'enhance_api_token',
|
|
];
|
|
|
|
public function index(): Response
|
|
{
|
|
$settings = [];
|
|
|
|
foreach (self::SETTING_DEFAULTS as $group => $keys) {
|
|
$stored = Setting::getGroup($group);
|
|
|
|
foreach ($keys as $key => $default) {
|
|
$value = $stored[$key] ?? $default;
|
|
$settings[$group][$key] = $value;
|
|
}
|
|
}
|
|
|
|
// Add read-only env-based values for display (masked)
|
|
$settings['api']['stripe_publishable_key'] = $this->maskValue(config('cashier.key'));
|
|
$settings['api']['stripe_secret_key'] = $this->maskValue(config('cashier.secret'));
|
|
$settings['api']['paypal_client_id'] = $this->maskValue(config('paypal.credentials.client_id'));
|
|
$settings['api']['paypal_client_secret'] = $this->maskValue(config('paypal.credentials.client_secret'));
|
|
|
|
// Mask sensitive stored values
|
|
foreach (self::SENSITIVE_KEYS as $key) {
|
|
foreach (self::SETTING_DEFAULTS as $group => $keys) {
|
|
if (array_key_exists($key, $keys) && ! empty($settings[$group][$key])) {
|
|
$settings[$group]["{$key}_masked"] = $this->maskValue($settings[$group][$key]);
|
|
$settings[$group]["{$key}_set"] = true;
|
|
} elseif (array_key_exists($key, $keys)) {
|
|
$settings[$group]["{$key}_masked"] = '';
|
|
$settings[$group]["{$key}_set"] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Inertia::render('Admin/Settings/Index', [
|
|
'settings' => $settings,
|
|
]);
|
|
}
|
|
|
|
public function update(UpdateSettingsRequest $request): RedirectResponse
|
|
{
|
|
$group = $request->validated('group');
|
|
$validated = $request->validated();
|
|
|
|
unset($validated['group']);
|
|
|
|
// For sensitive keys, skip if the value wasn't actually changed (empty means "keep current")
|
|
foreach (self::SENSITIVE_KEYS as $sensitiveKey) {
|
|
if (array_key_exists($sensitiveKey, $validated) && empty($validated[$sensitiveKey])) {
|
|
unset($validated[$sensitiveKey]);
|
|
}
|
|
}
|
|
|
|
Setting::setGroup($group, $validated);
|
|
|
|
return redirect()
|
|
->route('admin.settings.index')
|
|
->with('success', ucfirst($group).' settings updated successfully.');
|
|
}
|
|
|
|
/**
|
|
* Mask a sensitive value, showing only the last 4 characters.
|
|
*/
|
|
private function maskValue(?string $value): string
|
|
{
|
|
if (empty($value)) {
|
|
return '';
|
|
}
|
|
|
|
$length = strlen($value);
|
|
|
|
if ($length <= 4) {
|
|
return str_repeat('*', $length);
|
|
}
|
|
|
|
return str_repeat('*', $length - 4).substr($value, -4);
|
|
}
|
|
}
|