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