feat(api): API-Token-Verwaltung & GET /events/{id}
Pro-User können in den Einstellungen (Tab „API") benannte Tokens erstellen
und widerrufen. Feature api_access im FeatureSeeder als Pro-Feature ergänzt.
Neuer GET /events/{id} Endpunkt im EventController.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
parent
6b92544611
commit
2dd3903a4e
|
|
@ -67,6 +67,16 @@ class EventController extends Controller
|
|||
], 201);
|
||||
}
|
||||
|
||||
public function show(Request $request, string $id): JsonResponse
|
||||
{
|
||||
$event = $request->user()->events()->with('contacts')->findOrFail($id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $event,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, string $id): JsonResponse
|
||||
{
|
||||
$event = $request->user()->events()->findOrFail($id);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ class Index extends Component
|
|||
public string $new_password = '';
|
||||
public string $new_password_confirmation = '';
|
||||
|
||||
// ── API-Token ─────────────────────────────────────────────────────────
|
||||
public string $newTokenName = '';
|
||||
public ?string $createdToken = null;
|
||||
|
||||
protected $listeners = [
|
||||
'smtp:test' => 'testSmtp',
|
||||
];
|
||||
|
|
@ -326,6 +330,31 @@ class Index extends Component
|
|||
&& ($this->smtp_password || isset(auth()->user()->settings['smtp_password'])));
|
||||
}
|
||||
|
||||
public function getApiTokensProperty()
|
||||
{
|
||||
return auth()->user()->tokens()->latest()->get();
|
||||
}
|
||||
|
||||
public function createApiToken(): void
|
||||
{
|
||||
$this->validate(['newTokenName' => 'required|string|max:80']);
|
||||
|
||||
$plain = \Illuminate\Support\Str::random(64);
|
||||
auth()->user()->tokens()->create([
|
||||
'token' => hash('sha256', $plain),
|
||||
'name' => $this->newTokenName,
|
||||
]);
|
||||
|
||||
$this->createdToken = $plain;
|
||||
$this->newTokenName = '';
|
||||
}
|
||||
|
||||
public function revokeApiToken(string $id): void
|
||||
{
|
||||
auth()->user()->tokens()->where('id', $id)->delete();
|
||||
$this->createdToken = null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$affiliate = auth()->user()->affiliate;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class FeatureSeeder extends Seeder
|
|||
['key' => 'ai_agent', 'label' => 'KI-Assistent', 'icon' => 'heroicon-o-sparkles', 'group' => $productivity, 'sort' => 3],
|
||||
['key' => 'calendar_sync', 'label' => 'Kalender-Synchronisierung', 'icon' => 'heroicon-o-arrow-path', 'group' => $integration, 'sort' => 1],
|
||||
['key' => 'automations', 'label' => 'Automationen', 'icon' => 'heroicon-o-bolt', 'group' => $integration, 'sort' => 2],
|
||||
['key' => 'api_access', 'label' => 'API-Zugang', 'icon' => 'heroicon-o-code-bracket', 'group' => $integration, 'sort' => 3],
|
||||
['key' => 'speed', 'label' => 'GPT-4o Modell (schneller & präziser)', 'icon' => 'heroicon-o-bolt', 'group' => $performance, 'sort' => 1],
|
||||
];
|
||||
|
||||
|
|
@ -69,7 +70,7 @@ class FeatureSeeder extends Seeder
|
|||
$freeFeatures = ['calendar', 'reminders', 'tasks', 'notes', 'contacts', 'ai_agent'];
|
||||
|
||||
// Features die nur Pro bekommt
|
||||
$proFeatures = ['calendar_sync', 'automations', 'speed'];
|
||||
$proFeatures = ['calendar_sync', 'automations', 'api_access', 'speed'];
|
||||
|
||||
// Bestehende Verknüpfungen leeren
|
||||
DB::table('feature_plan')->delete();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
'smtp' => ['label' => t('settings.tab.smtp'), 'icon' => 'envelope'],
|
||||
'credits' => ['label' => t('settings.tab.credits'), 'icon' => 'bolt'],
|
||||
...(!auth()->user()->isInternalUser() ? ['affiliate' => ['label' => 'Affiliate', 'icon' => 'gift']] : []),
|
||||
'api' => ['label' => 'API', 'icon' => 'code-bracket'],
|
||||
'account' => ['label' => t('settings.tab.account'), 'icon' => 'shield-exclamation'],
|
||||
] as $key => $tab)
|
||||
<button wire:click="$set('activeTab', '{{ $key }}')"
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
@elseif($tab['icon'] === 'envelope') <x-heroicon-o-envelope class="w-3.5 h-3.5"/>
|
||||
@elseif($tab['icon'] === 'bolt') <x-heroicon-o-bolt class="w-3.5 h-3.5"/>
|
||||
@elseif($tab['icon'] === 'gift') <x-heroicon-o-gift class="w-3.5 h-3.5"/>
|
||||
@elseif($tab['icon'] === 'code-bracket') <x-heroicon-o-code-bracket class="w-3.5 h-3.5"/>
|
||||
@elseif($tab['icon'] === 'shield-exclamation') <x-heroicon-o-shield-exclamation class="w-3.5 h-3.5"/>
|
||||
@endif
|
||||
{{ $tab['label'] }}
|
||||
|
|
@ -658,6 +660,110 @@
|
|||
@endif
|
||||
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════ --}}
|
||||
{{-- TAB: API --}}
|
||||
{{-- ════════════════════════════════════════════════════════════ --}}
|
||||
<div x-show="$wire.activeTab === 'api'" x-cloak>
|
||||
<div class="bg-white border border-gray-100 rounded-xl shadow-sm">
|
||||
|
||||
<div class="flex items-center gap-3 px-6 py-5 border-b border-gray-100">
|
||||
<div class="w-9 h-9 rounded-lg bg-indigo-50 flex items-center justify-center">
|
||||
<x-heroicon-o-code-bracket class="w-4 h-4 text-indigo-500"/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-800">API-Zugang</h3>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Erstelle persönliche Tokens für den Zugriff auf die Aziros-API.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
|
||||
{{-- Neuen Token erstellen --}}
|
||||
@if(auth()->user()->hasFeature('api_access'))
|
||||
<div>
|
||||
<label class="{{ $labelClass }}">Neuen Token erstellen</label>
|
||||
<div class="flex gap-2">
|
||||
<input wire:model="newTokenName"
|
||||
placeholder="z.B. Mein Skript, Zapier…"
|
||||
class="{{ $inputClass }} flex-1"
|
||||
wire:keydown.enter="createApiToken"/>
|
||||
<button wire:click="createApiToken"
|
||||
class="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition-colors shrink-0">
|
||||
Erstellen
|
||||
</button>
|
||||
</div>
|
||||
@error('newTokenName') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
@if($createdToken)
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4 space-y-2">
|
||||
<p class="text-xs font-medium text-green-800">Token wurde erstellt — kopiere ihn jetzt, er wird nur einmal angezeigt.</p>
|
||||
<div class="flex gap-2 items-center" x-data="{ copied: false }">
|
||||
<code class="flex-1 text-xs bg-white border border-green-200 rounded px-3 py-2 text-gray-800 break-all font-mono select-all">{{ $createdToken }}</code>
|
||||
<button @click="navigator.clipboard.writeText('{{ $createdToken }}'); copied = true; setTimeout(() => copied = false, 2000)"
|
||||
class="px-3 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 shrink-0">
|
||||
<span x-show="!copied">Kopieren</span>
|
||||
<span x-show="copied" x-cloak>✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="flex items-center gap-3 p-4 bg-indigo-50 rounded-lg">
|
||||
<x-heroicon-o-lock-closed class="w-5 h-5 text-indigo-400 shrink-0"/>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-indigo-800">Pro-Feature</p>
|
||||
<p class="text-xs text-indigo-600 mt-0.5">API-Tokens sind für Pro-Nutzer verfügbar.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Bestehende Tokens --}}
|
||||
@if($this->apiTokens->isNotEmpty())
|
||||
<div>
|
||||
<label class="{{ $labelClass }}">Aktive Tokens</label>
|
||||
<div class="border border-gray-100 rounded-lg overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-100 text-left text-xs text-gray-500 uppercase tracking-wider">
|
||||
<th class="px-4 py-2.5">Name</th>
|
||||
<th class="px-4 py-2.5">Erstellt</th>
|
||||
<th class="px-4 py-2.5">Zuletzt verwendet</th>
|
||||
<th class="px-4 py-2.5"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
@foreach($this->apiTokens as $token)
|
||||
<tr class="hover:bg-gray-50/50">
|
||||
<td class="px-4 py-3 font-medium text-gray-800">{{ $token->name ?? 'Unbenannt' }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500">{{ $token->created_at->format('d.m.Y') }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500">{{ $token->last_used_at?->diffForHumans() ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<button wire:click="revokeApiToken('{{ $token->id }}')"
|
||||
wire:confirm="Token widerrufen?"
|
||||
class="text-xs text-red-500 hover:text-red-700 font-medium">
|
||||
Widerrufen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- API-Referenz --}}
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-2">
|
||||
<p class="text-xs font-medium text-gray-700">Verwendung</p>
|
||||
<code class="block text-xs text-gray-600 font-mono">Authorization: Bearer <dein-token></code>
|
||||
<code class="block text-xs text-gray-500 font-mono">GET https://api.aziros.com/v1/events?from=2026-01-01&to=2026-12-31</code>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════ --}}
|
||||
{{-- TAB: GEFAHRENZONE --}}
|
||||
{{-- ════════════════════════════════════════════════════════════ --}}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ Route::prefix('v1')->group(function () {
|
|||
// Kalender
|
||||
Route::get('/events', [EventController::class, 'index']);
|
||||
Route::post('/events', [EventController::class, 'store']);
|
||||
Route::get('/events/{id}', [EventController::class, 'show']);
|
||||
Route::put('/events/{id}', [EventController::class, 'update']);
|
||||
Route::delete('/events/{id}', [EventController::class, 'destroy']);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue