- Fixed database relationships: LlmRequest now properly uses gateway_user_id instead of user_id - Updated Models: GatewayUser and LlmRequest relationships corrected - Removed User->llmRequests relationship (admin users don't have LLM requests) - Simplified Dashboard: Now shows Gateway User statistics instead of admin users - Removed obsolete Budgets management pages (budgets handled directly in gateway_users) - Removed User Budgets admin section (redundant with gateway_users management) - Fixed view errors: Added null-checks for user_id in keys views - Updated navigation: Removed Budget and User Budget links - Updated routes: Cleaned up unused BudgetController and UserManagementController routes - Simplified StatisticsService: Focus on gateway_users and basic metrics only
263 lines
17 KiB
PHP
263 lines
17 KiB
PHP
<x-app-layout>
|
|
<x-slot name="header">
|
|
<div class="flex justify-between items-center">
|
|
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
|
{{ __('API Keys Management') }}
|
|
</h2>
|
|
<a href="{{ route('keys.create') }}"
|
|
class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
</svg>
|
|
Create New Key
|
|
</a>
|
|
</div>
|
|
</x-slot>
|
|
|
|
<div class="py-12">
|
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
<!-- Success/Error Messages -->
|
|
@if (session('success'))
|
|
<div class="mb-4 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative" role="alert">
|
|
<span class="block sm:inline">{{ session('success') }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
@if (session('error'))
|
|
<div class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
|
|
<span class="block sm:inline">{{ session('error') }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- New API Key Display (only shown once) -->
|
|
@if (session('new_api_key'))
|
|
<div class="mb-6 bg-yellow-50 border-l-4 border-yellow-400 p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-yellow-800">Save this API Key!</h3>
|
|
<div class="mt-2 text-sm text-yellow-700">
|
|
<p>This is the only time you'll see this key. Copy it now:</p>
|
|
<div class="mt-2 flex items-center">
|
|
<code id="new-api-key" class="bg-white px-4 py-2 rounded border border-yellow-300 font-mono text-sm">{{ session('new_api_key') }}</code>
|
|
<button onclick="copyToClipboard('new-api-key', event)"
|
|
class="ml-2 px-3 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600">
|
|
Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Filters -->
|
|
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg mb-6">
|
|
<div class="p-6">
|
|
<form method="GET" action="{{ route('keys.index') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<!-- Search -->
|
|
<div>
|
|
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">Search</label>
|
|
<input type="text"
|
|
name="search"
|
|
id="search"
|
|
value="{{ request('search') }}"
|
|
placeholder="Key name..."
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
|
|
</div>
|
|
|
|
<!-- Status Filter -->
|
|
<div>
|
|
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
|
<select name="status"
|
|
id="status"
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
|
|
<option value="">All</option>
|
|
<option value="active" {{ request('status') == 'active' ? 'selected' : '' }}>Active</option>
|
|
<option value="expired" {{ request('status') == 'expired' ? 'selected' : '' }}>Expired</option>
|
|
<option value="inactive" {{ request('status') == 'inactive' ? 'selected' : '' }}>Inactive</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- User Filter -->
|
|
<div>
|
|
<label for="user_id" class="block text-sm font-medium text-gray-700 mb-1">User</label>
|
|
<select name="user_id"
|
|
id="user_id"
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
|
|
<option value="">All Users</option>
|
|
@foreach($gatewayUsers as $user)
|
|
<option value="{{ $user->user_id }}" {{ request('user_id') == $user->user_id ? 'selected' : '' }}>
|
|
{{ $user->alias ?? $user->user_id }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Submit -->
|
|
<div class="flex items-end">
|
|
<button type="submit"
|
|
class="w-full inline-flex justify-center items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
|
Filter
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Keys Table -->
|
|
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
|
<div class="p-6 text-gray-900">
|
|
@if($apiKeys->count() > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Key Name
|
|
</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
User
|
|
</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Masked Key
|
|
</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Last Used
|
|
</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Created
|
|
</th>
|
|
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
@foreach($apiKeys as $key)
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-medium text-gray-900">
|
|
{{ $key->key_name }}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-900">
|
|
@if($key->user_id)
|
|
<a href="{{ route('gateway-users.show', $key->user_id) }}"
|
|
class="text-blue-600 hover:text-blue-900">
|
|
{{ $key->gatewayUser->alias ?? $key->user_id }}
|
|
</a>
|
|
@else
|
|
<span class="text-gray-400">No user</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<code class="text-xs text-gray-600 bg-gray-100 px-2 py-1 rounded">
|
|
{{ $key->masked_key }}
|
|
</code>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
@if($key->is_active && !$key->is_expired)
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
|
Active
|
|
</span>
|
|
@elseif($key->is_expired)
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
|
Expired
|
|
</span>
|
|
@else
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
|
Revoked
|
|
</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{{ $key->last_used_at ? $key->last_used_at->diffForHumans() : 'Never' }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{{ $key->created_at->format('Y-m-d H:i') }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<a href="{{ route('keys.show', $key->token) }}"
|
|
class="text-blue-600 hover:text-blue-900 mr-3">View</a>
|
|
@if($key->is_active && !$key->is_expired)
|
|
<form action="{{ route('keys.revoke', $key->token) }}"
|
|
method="POST"
|
|
class="inline"
|
|
onsubmit="return confirm('Are you sure you want to revoke this API key? This action cannot be undone.');">
|
|
@csrf
|
|
<button type="submit" class="text-red-600 hover:text-red-900">
|
|
Revoke
|
|
</button>
|
|
</form>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div class="mt-4">
|
|
{{ $apiKeys->links() }}
|
|
</div>
|
|
@else
|
|
<div class="text-center py-12">
|
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
|
</svg>
|
|
<h3 class="mt-2 text-sm font-medium text-gray-900">No API keys found</h3>
|
|
<p class="mt-1 text-sm text-gray-500">Get started by creating a new API key.</p>
|
|
<div class="mt-6">
|
|
<a href="{{ route('keys.create') }}"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
|
<svg class="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
</svg>
|
|
New API Key
|
|
</a>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@push('scripts')
|
|
<script>
|
|
function copyToClipboard(elementId, event) {
|
|
const element = document.getElementById(elementId);
|
|
const text = element.textContent;
|
|
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
// Show success message
|
|
const btn = event.target;
|
|
const originalText = btn.textContent;
|
|
btn.textContent = 'Copied!';
|
|
btn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600');
|
|
btn.classList.add('bg-green-500');
|
|
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.classList.remove('bg-green-500');
|
|
btn.classList.add('bg-yellow-500', 'hover:bg-yellow-600');
|
|
}, 2000);
|
|
}).catch(err => {
|
|
alert('Failed to copy: ' + err);
|
|
});
|
|
}
|
|
</script>
|
|
@endpush
|
|
</x-app-layout>
|