Files

187 lines
9.2 KiB
PHP

<?php
namespace App\Livewire\Admin;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithFileUploads;
class Cms extends Component
{
use WithFileUploads;
public string $section = 'pages';
public array $form = [];
public $upload;
#[Locked]
public ?int $editing = null;
private const TABLES = ['pages', 'releases', 'users', 'purchases', 'licenses', 'tickets', 'privacy_requests', 'consents', 'audit_logs', 'settings', 'navigation', 'media', 'redirects', 'email_templates'];
public function boot(): void
{
abort_unless(auth()->user()?->is_admin && auth()->user()->two_factor_confirmed_at && auth()->user()->hasVerifiedEmail(), 403);
}
private function section(): string
{
abort_unless(in_array($this->section, self::TABLES), 404);
return $this->section;
}
public function newRecord(): void
{
$this->reset('form', 'editing', 'upload');
}
public function updatedSection(): void
{
$this->reset('form', 'editing', 'upload');
}
private function audit(string $action, string $resource): void
{
DB::table('audit_logs')->insert(['user_id' => auth()->id(), 'action' => $action, 'resource' => $resource, 'created_at' => now(), 'updated_at' => now()]);
}
public function edit(int $id): void
{
$section = $this->section();
abort_unless(in_array($section, ['pages', 'releases']), 403);
$row = DB::table($section)->find($id);
abort_unless($row, 404);
$this->editing = $id;
$this->form = array_intersect_key((array) $row, array_flip($section === 'pages' ? ['slug', 'title', 'body', 'description', 'published'] : ['version', 'channel', 'platform', 'notes', 'published']));
}
public function save(): void
{
$section = $this->section();
abort_unless(in_array($section, ['pages', 'releases']), 403);
if ($section === 'pages') {
$data = $this->validate(['form.slug' => 'required|alpha_dash|max:100|unique:pages,slug,'.($this->editing ?? 'NULL'), 'form.title' => 'required|string|max:200', 'form.body' => 'required|string|max:100000', 'form.description' => 'nullable|string|max:255', 'form.published' => 'boolean'])['form'];
} else {
$data = $this->validate(['form.version' => 'required|regex:/^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.]+)?$/|max:50|unique:releases,version,'.($this->editing ?? 'NULL'), 'form.channel' => 'required|in:stable,beta', 'form.platform' => 'required|string|max:50', 'form.notes' => 'required|string|max:20000', 'form.published' => 'boolean'])['form'];
$this->validate(['upload' => ($this->editing ? 'nullable' : 'required').'|file|extensions:zip,dmg,pkg|max:1048576']);
if ($this->upload) {
$path = $this->upload->store('releases', 'local');
$data += ['path' => $path, 'sha256' => hash_file('sha256', Storage::disk('local')->path($path)), 'bytes' => Storage::disk('local')->size($path)];
}
}
$data['published'] = $data['published'] ?? false;
$data['updated_at'] = now();
if ($this->editing) {
DB::table($section)->where('id', $this->editing)->update($data);
$id = $this->editing;
} else {
$data['created_at'] = now();
$id = DB::table($section)->insertGetId($data);
}
$this->audit('saved', $section.':'.$id);
$this->reset('form', 'editing', 'upload');
session()->flash('status', 'Saved successfully.');
}
public function revoke(int $id): void
{
abort_unless($this->section() === 'licenses', 403);
DB::table('licenses')->where('id', $id)->update(['status' => 'revoked', 'updated_at' => now()]);
$this->audit('revoked', 'license:'.$id);
}
public function saveResource(): void
{
$section = $this->section();
abort_unless(in_array($section, ['navigation', 'media', 'redirects', 'email_templates']), 403);
$rules = match ($section) {
'navigation' => ['form.label' => 'required|string|max:80', 'form.path' => ['required', 'string', 'regex:~^/(?!/)[a-zA-Z0-9/_?.=&%#-]*$~'], 'form.position' => 'required|integer|min:0|max:1000', 'form.published' => 'boolean'],
'redirects' => ['form.source' => ['required', 'string', 'max:200', 'regex:~^/(?!/)[a-zA-Z0-9/_-]+$~', 'unique:redirects,source'], 'form.destination' => ['required', 'string', 'max:200', 'different:form.source', 'regex:~^/(?!/)[a-zA-Z0-9/_-]+$~']],
'media' => ['form.title' => 'required|string|max:150', 'form.alt' => 'required|string|max:255', 'upload' => 'required|image|mimes:png,jpg,jpeg,webp|dimensions:max_width=6000,max_height=6000|max:10240'],
'email_templates' => ['form.slug' => 'required|alpha_dash|max:80|unique:email_templates,slug', 'form.subject' => 'required|string|max:200', 'form.body' => 'required|string|max:10000'],
};
$validated = $this->validate($rules);
$data = $validated['form'];
if ($section === 'media') {
$data += ['path' => $this->upload->store('media', 'local'), 'mime' => $this->upload->getMimeType(), 'bytes' => $this->upload->getSize()];
}
if ($section === 'navigation') {
$data['published'] = $data['published'] ?? false;
}
DB::table($section)->insert([...$data, 'created_at' => now(), 'updated_at' => now()]);
$this->audit('created', $section);
$this->reset('form', 'upload');
}
public function remove(int $id): void
{
$section = $this->section();
abort_unless(in_array($section, ['navigation', 'media', 'redirects', 'email_templates']), 403);
$row = DB::table($section)->find($id);
abort_unless($row, 404);
DB::table($section)->where('id', $id)->delete();
if ($section === 'media') {
Storage::disk('local')->delete($row->path);
}$this->audit('deleted', $section.':'.$id);
}
public function setDeviceLimit(int $id): void
{
abort_unless($this->section() === 'licenses', 403);
$v = $this->validate(['form.device_limit' => 'required|integer|min:1|max:100']);
DB::table('licenses')->where('id', $id)->update(['device_limit' => $v['form']['device_limit'], 'updated_at' => now()]);
$this->audit('device limit updated', 'license:'.$id);
}
public function saveSettings(): void
{
abort_unless($this->section() === 'settings', 403);
$v = $this->validate(['form.headline' => 'required|string|max:120', 'form.tagline' => 'required|string|max:500', 'form.price_label' => 'required|string|max:100'])['form'];
foreach ($v as $key => $value) {
DB::table('settings')->updateOrInsert(['key' => $key], ['value' => $value]);
}$this->audit('saved', 'settings');
session()->flash('status', 'Settings saved.');
}
public function fulfillDeletion(int $id): void
{
abort_unless($this->section() === 'privacy_requests', 403);
DB::transaction(function () use ($id) {
$request = DB::table('privacy_requests')->where('id', $id)->lockForUpdate()->first();
abort_unless($request && $request->type === 'deletion' && $request->status === 'pending', 422);
abort_if($request->user_id === auth()->id(), 422, 'Cannot erase your own administrator account.');
$user = User::findOrFail($request->user_id);
abort_if($user->is_admin, 422, 'Remove administrator access before erasure.');
DB::table('licenses')->where('user_id', $user->id)->update(['status' => 'revoked', 'updated_at' => now()]);
DB::table('activations')->whereIn('license_id', DB::table('licenses')->where('user_id', $user->id)->select('id'))->delete();
DB::table('tickets')->where('user_id', $user->id)->delete();
DB::table('ticket_messages')->where('user_id', $user->id)->delete();
DB::table('consents')->where('user_id', $user->id)->delete();
DB::table('sessions')->where('user_id', $user->id)->delete();
DB::table('password_reset_tokens')->where('email', $user->email)->delete();
$user->forceFill(['name' => 'Deleted customer', 'email' => 'deleted-'.$user->id.'@invalid.example', 'password' => bin2hex(random_bytes(32)), 'remember_token' => null, 'account_token' => null, 'two_factor_secret' => null, 'two_factor_recovery_codes' => null])->save();
DB::table('privacy_requests')->where('id', $id)->update(['status' => 'fulfilled', 'updated_at' => now()]);
$this->audit('privacy erasure fulfilled; billing records retained', 'user:'.$user->id);
});
}
public function render()
{
$section = $this->section();
$query = DB::table($section);
if ($section === 'users') {
$query->select('id', 'name', 'email', 'is_admin', 'email_verified_at');
} elseif ($section === 'licenses') {
$query->select('id','user_id','purchase_id','status','device_limit');
}
return view('livewire.admin.cms',['sections' => self::TABLES, 'rows' => $query->orderBy($section === 'settings' ? 'key' : 'id','desc')->limit(100)->get()]);
}
}