mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-18 04:50:30 +00:00
✨ Add Super-Admin tools for managing any model
- 🛠️ Introduced generic Super-Admin MCP tools, including `list-models`, `describe-model`, `list-records`, `show-record`, `create-record`, and `update-record`. - 🛡️ Restricted modification of critical fields (e.g., passwords, roles, tokens) to enhance security. - ✅ Added extensive feature tests for Super-Admin functionality and access control. - 📜 Increased pagination length to accommodate new tools on a single page. - 🔗 Registered Super-Admin tools in `EinundzwanzigServer`.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Support;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* Registry aller bearbeitbaren Eloquent-Models unter App\Models. Dient den generischen
|
||||
* Super-Admin-MCP-Tools, damit ein Super-Admin jede Entität per Name ansprechen kann –
|
||||
* ohne pro Model ein eigenes Tool zu pflegen.
|
||||
*/
|
||||
class SuperAdminModels
|
||||
{
|
||||
/**
|
||||
* Attribute, die niemals über die generischen Super-Admin-Tools geschrieben werden
|
||||
* dürfen: Passwörter & Rollen, Auth-Credentials/-Tokens (remember_token, two_factor_*,
|
||||
* OAuth token/refresh_token/secret, lnurl-auth k1), die E-Mail-Verifizierung
|
||||
* (email_verified_at) sowie der Nostr-Pubkey (Identitäts-Spoofing). lnurl und
|
||||
* reputation bleiben bewusst editierbar.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public const PROTECTED_ATTRIBUTES = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_confirmed_at',
|
||||
'role',
|
||||
'roles',
|
||||
'token',
|
||||
'refresh_token',
|
||||
'secret',
|
||||
'k1',
|
||||
'email_verified_at',
|
||||
'nostr',
|
||||
];
|
||||
|
||||
/**
|
||||
* Kanonische Map Modelklasse → Tabellenname. Einmal pro Prozess ermittelt.
|
||||
*
|
||||
* @var array<class-string<Model>, string>|null
|
||||
*/
|
||||
private static ?array $models = null;
|
||||
|
||||
/**
|
||||
* Normalisierter Name (Kurzname ODER Tabelle) → Modelklasse. Lazy aus $models gebaut.
|
||||
*
|
||||
* @var array<string, class-string<Model>>|null
|
||||
*/
|
||||
private static ?array $index = null;
|
||||
|
||||
/**
|
||||
* Liste aller bearbeitbaren Models.
|
||||
*
|
||||
* @return array<int, array{key: string, class: class-string<Model>, table: string}>
|
||||
*/
|
||||
public static function list(): array
|
||||
{
|
||||
$models = [];
|
||||
|
||||
foreach (self::models() as $class => $table) {
|
||||
$models[] = [
|
||||
'key' => Str::kebab(class_basename($class)),
|
||||
'class' => $class,
|
||||
'table' => $table,
|
||||
];
|
||||
}
|
||||
|
||||
usort($models, fn (array $a, array $b): int => $a['key'] <=> $b['key']);
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst einen Model-Namen (Kurzname, FQCN, kebab-case oder Tabellenname) zur Klasse auf.
|
||||
*
|
||||
* @return class-string<Model>|null
|
||||
*/
|
||||
public static function resolve(?string $name): ?string
|
||||
{
|
||||
if (! is_string($name) || trim($name) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::index()[self::normalize(Str::of($name)->afterLast('\\')->value())] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verfügbare Model-Keys (kebab-case) als Auswahlhilfe.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function keys(): array
|
||||
{
|
||||
return array_map(fn (array $m): string => $m['key'], self::list());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt einmalig alle ladbaren, konkreten Models unter App\Models samt Tabellenname.
|
||||
*
|
||||
* @return array<class-string<Model>, string>
|
||||
*/
|
||||
private static function models(): array
|
||||
{
|
||||
if (self::$models !== null) {
|
||||
return self::$models;
|
||||
}
|
||||
|
||||
$models = [];
|
||||
|
||||
foreach (glob(app_path('Models').'/*.php') ?: [] as $file) {
|
||||
$class = 'App\\Models\\'.basename($file, '.php');
|
||||
|
||||
try {
|
||||
// Manche Model-Stubs erben von nicht (mehr) installierten Vendor-Klassen
|
||||
// (z. B. Jetstream) und lassen sich nicht laden – diese überspringen wir.
|
||||
if (! class_exists($class) || ! is_subclass_of($class, Model::class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((new ReflectionClass($class))->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$models[$class] = (new $class)->getTable();
|
||||
} catch (\Throwable) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$models = $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den Auflösungs-Index: Kurzname UND Tabellenname (jeweils normalisiert) → Klasse.
|
||||
*
|
||||
* @return array<string, class-string<Model>>
|
||||
*/
|
||||
private static function index(): array
|
||||
{
|
||||
if (self::$index !== null) {
|
||||
return self::$index;
|
||||
}
|
||||
|
||||
$index = [];
|
||||
|
||||
foreach (self::models() as $class => $table) {
|
||||
$index[self::normalize(class_basename($class))] = $class;
|
||||
$index[self::normalize($table)] = $class;
|
||||
}
|
||||
|
||||
return self::$index = $index;
|
||||
}
|
||||
|
||||
private static function normalize(string $value): string
|
||||
{
|
||||
return Str::of($value)->lower()->replace(['-', '_', ' '], '')->value();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user