Files
website/website/app/Http/Controllers/Admin/SettingsController.php
Claude Dev 813fde30c2 Add admin audit log viewer and system settings
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>
2026-02-09 10:45:31 -05:00

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);
}
}