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);
|
], 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
|
public function update(Request $request, string $id): JsonResponse
|
||||||
{
|
{
|
||||||
$event = $request->user()->events()->findOrFail($id);
|
$event = $request->user()->events()->findOrFail($id);
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,10 @@ class Index extends Component
|
||||||
public string $new_password = '';
|
public string $new_password = '';
|
||||||
public string $new_password_confirmation = '';
|
public string $new_password_confirmation = '';
|
||||||
|
|
||||||
|
// ── API-Token ─────────────────────────────────────────────────────────
|
||||||
|
public string $newTokenName = '';
|
||||||
|
public ?string $createdToken = null;
|
||||||
|
|
||||||
protected $listeners = [
|
protected $listeners = [
|
||||||
'smtp:test' => 'testSmtp',
|
'smtp:test' => 'testSmtp',
|
||||||
];
|
];
|
||||||
|
|
@ -326,6 +330,31 @@ class Index extends Component
|
||||||
&& ($this->smtp_password || isset(auth()->user()->settings['smtp_password'])));
|
&& ($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()
|
public function render()
|
||||||
{
|
{
|
||||||
$affiliate = auth()->user()->affiliate;
|
$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' => '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' => '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' => '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],
|
['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'];
|
$freeFeatures = ['calendar', 'reminders', 'tasks', 'notes', 'contacts', 'ai_agent'];
|
||||||
|
|
||||||
// Features die nur Pro bekommt
|
// Features die nur Pro bekommt
|
||||||
$proFeatures = ['calendar_sync', 'automations', 'speed'];
|
$proFeatures = ['calendar_sync', 'automations', 'api_access', 'speed'];
|
||||||
|
|
||||||
// Bestehende Verknüpfungen leeren
|
// Bestehende Verknüpfungen leeren
|
||||||
DB::table('feature_plan')->delete();
|
DB::table('feature_plan')->delete();
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
'smtp' => ['label' => t('settings.tab.smtp'), 'icon' => 'envelope'],
|
'smtp' => ['label' => t('settings.tab.smtp'), 'icon' => 'envelope'],
|
||||||
'credits' => ['label' => t('settings.tab.credits'), 'icon' => 'bolt'],
|
'credits' => ['label' => t('settings.tab.credits'), 'icon' => 'bolt'],
|
||||||
...(!auth()->user()->isInternalUser() ? ['affiliate' => ['label' => 'Affiliate', 'icon' => 'gift']] : []),
|
...(!auth()->user()->isInternalUser() ? ['affiliate' => ['label' => 'Affiliate', 'icon' => 'gift']] : []),
|
||||||
|
'api' => ['label' => 'API', 'icon' => 'code-bracket'],
|
||||||
'account' => ['label' => t('settings.tab.account'), 'icon' => 'shield-exclamation'],
|
'account' => ['label' => t('settings.tab.account'), 'icon' => 'shield-exclamation'],
|
||||||
] as $key => $tab)
|
] as $key => $tab)
|
||||||
<button wire:click="$set('activeTab', '{{ $key }}')"
|
<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'] === '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'] === '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'] === '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"/>
|
@elseif($tab['icon'] === 'shield-exclamation') <x-heroicon-o-shield-exclamation class="w-3.5 h-3.5"/>
|
||||||
@endif
|
@endif
|
||||||
{{ $tab['label'] }}
|
{{ $tab['label'] }}
|
||||||
|
|
@ -658,6 +660,110 @@
|
||||||
@endif
|
@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 --}}
|
{{-- TAB: GEFAHRENZONE --}}
|
||||||
{{-- ════════════════════════════════════════════════════════════ --}}
|
{{-- ════════════════════════════════════════════════════════════ --}}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ Route::prefix('v1')->group(function () {
|
||||||
// Kalender
|
// Kalender
|
||||||
Route::get('/events', [EventController::class, 'index']);
|
Route::get('/events', [EventController::class, 'index']);
|
||||||
Route::post('/events', [EventController::class, 'store']);
|
Route::post('/events', [EventController::class, 'store']);
|
||||||
|
Route::get('/events/{id}', [EventController::class, 'show']);
|
||||||
Route::put('/events/{id}', [EventController::class, 'update']);
|
Route::put('/events/{id}', [EventController::class, 'update']);
|
||||||
Route::delete('/events/{id}', [EventController::class, 'destroy']);
|
Route::delete('/events/{id}', [EventController::class, 'destroy']);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue