- Any-LLM Gateway setup with Docker Compose - Laravel 11 admin interface with Livewire - Dashboard with usage statistics and charts - Gateway Users management with budget tracking - API Keys management with revocation - Budget templates with assignment - Usage Logs with filtering and CSV export - Model Pricing management with calculator - PostgreSQL database integration - Complete authentication system for admins
315 lines
16 KiB
PHP
315 lines
16 KiB
PHP
<x-app-layout>
|
|
<x-slot name="header">
|
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
|
{{ __('Gateway Users') }}
|
|
</h2>
|
|
</x-slot>
|
|
|
|
<div class="py-12">
|
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
<!-- Header -->
|
|
<div class="mb-6 flex justify-between items-center">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">Gateway Users</h1>
|
|
<p class="mt-1 text-sm text-gray-600">Manage API consumers and their access</p>
|
|
</div>
|
|
<a href="{{ route('gateway-users.create') }}"
|
|
class="inline-flex items-center px-4 py-2 bg-indigo-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-indigo-700">
|
|
<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"></path>
|
|
</svg>
|
|
Create User
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Success Message -->
|
|
@if(session('success'))
|
|
<div class="mb-6 bg-green-50 border-l-4 border-green-400 p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm text-green-700">{{ session('success') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Filters -->
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<form method="GET" action="{{ route('gateway-users.index') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4" id="filterForm">
|
|
<div>
|
|
<label for="search" class="block text-sm font-medium text-gray-700">Search</label>
|
|
<input type="text"
|
|
name="search"
|
|
id="search"
|
|
value="{{ request('search') }}"
|
|
placeholder="User ID or Alias..."
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
</div>
|
|
|
|
<div>
|
|
<label for="status" class="block text-sm font-medium text-gray-700">Status</label>
|
|
<select name="status"
|
|
id="status"
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
<option value="">All Users</option>
|
|
<option value="active" {{ request('status') == 'active' ? 'selected' : '' }}>Active</option>
|
|
<option value="blocked" {{ request('status') == 'blocked' ? 'selected' : '' }}>Blocked</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="sort" class="block text-sm font-medium text-gray-700">Sort By</label>
|
|
<select name="sort"
|
|
id="sort"
|
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
<option value="created_at" {{ request('sort') == 'created_at' ? 'selected' : '' }}>Created Date</option>
|
|
<option value="spend" {{ request('sort') == 'spend' ? 'selected' : '' }}>Spend</option>
|
|
<option value="alias" {{ request('sort') == 'alias' ? 'selected' : '' }}>Alias</option>
|
|
</select>
|
|
</div>
|
|
|
|
<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">
|
|
Apply Filters
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Bulk Actions Bar (hidden by default) -->
|
|
<div id="bulkActionsBar" class="bg-indigo-50 border border-indigo-200 rounded-lg p-4 mb-6 hidden">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<span class="text-sm font-medium text-gray-700 mr-4">
|
|
<span id="selectedCount">0</span> user(s) selected
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center space-x-3">
|
|
<form id="bulkActionForm" method="POST" action="{{ route('gateway-users.bulk-action') }}" class="flex items-center space-x-3">
|
|
@csrf
|
|
<input type="hidden" name="action" id="bulkActionType">
|
|
<div id="selectedUserIds"></div>
|
|
|
|
<button type="button"
|
|
onclick="executeBulkAction('block')"
|
|
class="inline-flex items-center px-3 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-700">
|
|
Block Selected
|
|
</button>
|
|
<button type="button"
|
|
onclick="executeBulkAction('unblock')"
|
|
class="inline-flex items-center px-3 py-2 bg-green-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-green-700">
|
|
Unblock Selected
|
|
</button>
|
|
<button type="button"
|
|
onclick="executeBulkAction('delete')"
|
|
class="inline-flex items-center px-3 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700">
|
|
Delete Selected
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Users Table -->
|
|
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left">
|
|
<input type="checkbox"
|
|
id="selectAll"
|
|
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
onchange="toggleAllUsers(this)">
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Budget</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Spend</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">API Keys</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Requests</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th 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">
|
|
@forelse($users as $user)
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<input type="checkbox"
|
|
class="user-checkbox rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
value="{{ $user->user_id }}"
|
|
onchange="updateBulkActions()">
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-indigo-100 rounded-full">
|
|
<span class="text-indigo-600 font-semibold text-sm">
|
|
{{ strtoupper(substr($user->alias ?? $user->user_id, 0, 2)) }}
|
|
</span>
|
|
</div>
|
|
<div class="ml-4">
|
|
<div class="text-sm font-medium text-gray-900">
|
|
{{ $user->alias ?? 'N/A' }}
|
|
</div>
|
|
<div class="text-sm text-gray-500 font-mono">
|
|
{{ substr($user->user_id, 0, 20) }}...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
@if($user->budget)
|
|
<div class="text-sm text-gray-900">{{ $user->budget->budget_id }}</div>
|
|
<div class="text-sm text-gray-500">${{ number_format($user->budget->max_budget, 2) }}</div>
|
|
@else
|
|
<span class="text-sm text-gray-400">No budget</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-semibold text-gray-900">${{ number_format($user->spend, 2) }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-900">{{ $user->api_keys_count }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-900">{{ number_format($user->usage_logs_count) }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
@if($user->blocked)
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
|
Blocked
|
|
</span>
|
|
@else
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
|
Active
|
|
</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<a href="{{ route('gateway-users.show', $user->user_id) }}"
|
|
class="text-indigo-600 hover:text-indigo-900 mr-3">View</a>
|
|
<a href="{{ route('gateway-users.edit', $user->user_id) }}"
|
|
class="text-yellow-600 hover:text-yellow-900 mr-3">Edit</a>
|
|
<form action="{{ route('gateway-users.toggle-block', $user->user_id) }}"
|
|
method="POST"
|
|
class="inline-block">
|
|
@csrf
|
|
<button type="submit"
|
|
class="text-{{ $user->blocked ? 'green' : 'red' }}-600 hover:text-{{ $user->blocked ? 'green' : 'red' }}-900">
|
|
{{ $user->blocked ? 'Unblock' : 'Block' }}
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="8" class="px-6 py-12 text-center">
|
|
<div class="flex flex-col items-center">
|
|
<svg class="w-16 h-16 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
|
</svg>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-1">No users found</h3>
|
|
<p class="text-sm text-gray-500 mb-4">Get started by creating your first gateway user.</p>
|
|
<a href="{{ route('gateway-users.create') }}"
|
|
class="inline-flex items-center px-4 py-2 bg-indigo-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-indigo-700">
|
|
Create User
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
@if($users->hasPages())
|
|
<div class="mt-6">
|
|
{{ $users->links() }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Bulk Actions JavaScript
|
|
function toggleAllUsers(checkbox) {
|
|
const userCheckboxes = document.querySelectorAll('.user-checkbox');
|
|
userCheckboxes.forEach(cb => {
|
|
cb.checked = checkbox.checked;
|
|
});
|
|
updateBulkActions();
|
|
}
|
|
|
|
function updateBulkActions() {
|
|
const checkedBoxes = document.querySelectorAll('.user-checkbox:checked');
|
|
const bulkActionsBar = document.getElementById('bulkActionsBar');
|
|
const selectedCount = document.getElementById('selectedCount');
|
|
const selectAll = document.getElementById('selectAll');
|
|
|
|
// Update count
|
|
selectedCount.textContent = checkedBoxes.length;
|
|
|
|
// Show/hide bulk actions bar
|
|
if (checkedBoxes.length > 0) {
|
|
bulkActionsBar.classList.remove('hidden');
|
|
} else {
|
|
bulkActionsBar.classList.add('hidden');
|
|
}
|
|
|
|
// Update "Select All" checkbox state
|
|
const allCheckboxes = document.querySelectorAll('.user-checkbox');
|
|
if (checkedBoxes.length === 0) {
|
|
selectAll.checked = false;
|
|
selectAll.indeterminate = false;
|
|
} else if (checkedBoxes.length === allCheckboxes.length) {
|
|
selectAll.checked = true;
|
|
selectAll.indeterminate = false;
|
|
} else {
|
|
selectAll.checked = false;
|
|
selectAll.indeterminate = true;
|
|
}
|
|
}
|
|
|
|
function executeBulkAction(action) {
|
|
const checkedBoxes = document.querySelectorAll('.user-checkbox:checked');
|
|
|
|
if (checkedBoxes.length === 0) {
|
|
alert('Please select at least one user.');
|
|
return;
|
|
}
|
|
|
|
// Confirmation for delete action
|
|
if (action === 'delete') {
|
|
if (!confirm(`Are you sure you want to delete ${checkedBoxes.length} user(s)? This will also delete all associated API keys and usage logs. This action cannot be undone!`)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Set action type
|
|
document.getElementById('bulkActionType').value = action;
|
|
|
|
// Add selected user IDs as hidden inputs
|
|
const selectedUserIds = document.getElementById('selectedUserIds');
|
|
selectedUserIds.innerHTML = '';
|
|
|
|
checkedBoxes.forEach(checkbox => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'user_ids[]';
|
|
input.value = checkbox.value;
|
|
selectedUserIds.appendChild(input);
|
|
});
|
|
|
|
// Submit form
|
|
document.getElementById('bulkActionForm').submit();
|
|
}
|
|
</script>
|
|
|
|
</x-app-layout>
|