You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
192 lines
11 KiB
192 lines
11 KiB
@extends('layouts.app') |
|
|
|
@section('title', 'API Clients — ' . config('app.name')) |
|
|
|
@section('content') |
|
<div class="max-w-[1600px] mx-auto px-4 py-6 sm:py-8"> |
|
<div class="mt-6 mb-8 flex flex-col md:flex-row md:items-center md:justify-between gap-4"> |
|
<div> |
|
<h1 class="text-3xl font-bold text-white">API Clients</h1> |
|
<p class="text-slate-400 mt-1 text-sm">Kelola API Token dan OAuth2 Client Credentials</p> |
|
</div> |
|
<a href="{{ route('api-docs.index') }}" class="text-sm text-cyan-400 hover:text-cyan-300">→ API Documentation</a> |
|
</div> |
|
|
|
@include('partials.flash') |
|
|
|
@if ($newToken) |
|
<div class="mb-6 rounded-lg bg-emerald-950/50 border border-emerald-800 p-4"> |
|
<p class="text-emerald-200 text-sm font-medium mb-2">New API Token: {{ $newToken['name'] }}</p> |
|
<code class="block bg-slate-950 border border-slate-800 rounded p-3 text-xs text-cyan-300 break-all select-all">{{ $newToken['token'] }}</code> |
|
<p class="text-emerald-400/70 text-xs mt-2">Salin sekarang. Token tidak ditampilkan lagi.</p> |
|
</div> |
|
@endif |
|
|
|
@if ($newClient) |
|
<div class="mb-6 rounded-lg bg-violet-950/50 border border-violet-800 p-4 space-y-2 text-sm"> |
|
<p class="text-violet-200 font-medium">New OAuth2 Client: {{ $newClient['name'] }}</p> |
|
<p class="text-slate-300">Client ID: <code class="text-cyan-300 select-all">{{ $newClient['client_id'] }}</code></p> |
|
<p class="text-slate-300">Client Secret: <code class="text-cyan-300 break-all select-all">{{ $newClient['client_secret'] }}</code></p> |
|
<p class="text-violet-400/70 text-xs">Salin sekarang. Secret tidak ditampilkan lagi.</p> |
|
</div> |
|
@endif |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl p-6"> |
|
<h2 class="text-lg font-semibold text-white mb-4">Create API Token</h2> |
|
<form method="POST" action="{{ route('api-clients.tokens.store') }}" class="space-y-4"> |
|
@csrf |
|
<div> |
|
<label class="block text-sm text-slate-300 mb-1">Name</label> |
|
<input name="name" required maxlength="100" placeholder="n8n-integration" |
|
class="w-full rounded-lg bg-slate-800 border border-slate-700 px-4 py-2 text-sm text-white"> |
|
</div> |
|
<div> |
|
<label class="block text-sm text-slate-300 mb-2">Scopes</label> |
|
<div class="space-y-2"> |
|
@foreach ($scopes as $key => $label) |
|
<label class="flex items-center gap-2 text-sm text-slate-300"> |
|
<input type="checkbox" name="scopes[]" value="{{ $key }}" class="rounded"> |
|
<span class="font-mono text-cyan-400">{{ $key }}</span> |
|
<span class="text-slate-500">— {{ $label }}</span> |
|
</label> |
|
@endforeach |
|
</div> |
|
</div> |
|
<div> |
|
<label class="block text-sm text-slate-300 mb-1">Expires (days, optional)</label> |
|
<input type="number" name="expires_in_days" min="1" max="3650" placeholder="kosong = tidak expire" |
|
class="w-full rounded-lg bg-slate-800 border border-slate-700 px-4 py-2 text-sm text-white"> |
|
</div> |
|
<button type="submit" class="rounded-lg bg-cyan-600 hover:bg-cyan-500 px-5 py-2 text-sm font-medium text-white"> |
|
Create Token |
|
</button> |
|
</form> |
|
</div> |
|
|
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl p-6"> |
|
<h2 class="text-lg font-semibold text-white mb-4">Create OAuth2 Client</h2> |
|
<form method="POST" action="{{ route('api-clients.oauth.store') }}" class="space-y-4"> |
|
@csrf |
|
<div> |
|
<label class="block text-sm text-slate-300 mb-1">Client Name</label> |
|
<input name="name" required maxlength="100" placeholder="partner-app" |
|
class="w-full rounded-lg bg-slate-800 border border-slate-700 px-4 py-2 text-sm text-white"> |
|
</div> |
|
<p class="text-slate-500 text-xs">Grant type: <code class="text-cyan-400">client_credentials</code>. Scope diminta saat request token.</p> |
|
<button type="submit" class="rounded-lg bg-violet-600 hover:bg-violet-500 px-5 py-2 text-sm font-medium text-white"> |
|
Create OAuth Client |
|
</button> |
|
</form> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-8 bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden flex flex-col list-panel"> |
|
<div class="list-panel__toolbar p-4 border-b border-slate-800"> |
|
<h2 class="font-semibold text-white">API Tokens ({{ count($apiTokens) }})</h2> |
|
</div> |
|
<div class="list-panel__scroll"> |
|
<table class="min-w-full text-sm list-table list-table--row-actions"> |
|
<thead class="text-slate-300"> |
|
<tr> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Name</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Prefix</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Scopes</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Last Used</th> |
|
<th class="list-col-actions px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Action</th> |
|
@include('partials.list-table-row-hint', ['tag' => 'th']) |
|
</tr> |
|
</thead> |
|
<tbody class="divide-y divide-slate-800"> |
|
@forelse ($apiTokens as $token) |
|
<tr data-list-row-title="{{ $token->name }}"> |
|
<td class="px-4 py-3 text-white">{{ $token->name }}</td> |
|
<td class="px-4 py-3 font-mono text-slate-400">{{ $token->token_prefix }}...</td> |
|
<td class="px-4 py-3 text-slate-300 text-xs">{{ implode(', ', $token->scopes ?? []) }}</td> |
|
<td class="px-4 py-3 text-slate-400">{{ $token->last_used_at?->format('Y-m-d H:i') ?? '—' }}</td> |
|
<td class="list-col-actions px-4 py-3"> |
|
<form method="POST" action="{{ route('api-clients.tokens.destroy', $token) }}" |
|
onsubmit="return confirm('Revoke token [{{ $token->name }}]?');"> |
|
@csrf |
|
@method('DELETE') |
|
<button class="rounded bg-red-800 hover:bg-red-700 text-white text-xs px-2 py-1">Revoke</button> |
|
</form> |
|
</td> |
|
@include('partials.list-table-row-hint', ['tag' => 'td']) |
|
</tr> |
|
@empty |
|
<tr><td colspan="6" class="px-4 py-8 text-center text-slate-400">No API tokens.</td></tr> |
|
@endforelse |
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-8 bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden flex flex-col list-panel"> |
|
<div class="list-panel__toolbar p-4 border-b border-slate-800"> |
|
<h2 class="font-semibold text-white">OAuth2 Clients ({{ $oauthClients->where('revoked', false)->count() }})</h2> |
|
</div> |
|
<div class="list-panel__scroll"> |
|
<table class="min-w-full text-sm list-table list-table--row-actions"> |
|
<thead class="text-slate-300"> |
|
<tr> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Name</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Client ID</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Status</th> |
|
<th class="list-col-actions px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Action</th> |
|
@include('partials.list-table-row-hint', ['tag' => 'th']) |
|
</tr> |
|
</thead> |
|
<tbody class="divide-y divide-slate-800"> |
|
@forelse ($oauthClients as $client) |
|
<tr class="{{ $client->revoked ? 'opacity-50' : '' }}" data-list-row-title="{{ $client->name }}"> |
|
<td class="px-4 py-3 text-white">{{ $client->name }}</td> |
|
<td class="px-4 py-3 font-mono text-xs text-slate-400">{{ $client->id }}</td> |
|
<td class="px-4 py-3"> |
|
<span class="text-xs {{ $client->revoked ? 'text-red-400' : 'text-emerald-400' }}"> |
|
{{ $client->revoked ? 'Revoked' : 'Active' }} |
|
</span> |
|
</td> |
|
<td class="list-col-actions px-4 py-3"> |
|
@unless ($client->revoked) |
|
<form method="POST" action="{{ route('api-clients.oauth.destroy', $client->id) }}" |
|
onsubmit="return confirm('Revoke OAuth client?');"> |
|
@csrf |
|
@method('DELETE') |
|
<button class="rounded bg-red-800 hover:bg-red-700 text-white text-xs px-2 py-1">Revoke</button> |
|
</form> |
|
@endunless |
|
</td> |
|
@include('partials.list-table-row-hint', ['tag' => 'td']) |
|
</tr> |
|
@empty |
|
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-400">No OAuth clients.</td></tr> |
|
@endforelse |
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
|
|
<aside class="mt-8 bg-slate-900 border border-slate-800 rounded-2xl p-5 notes-panel"> |
|
<h2 class="text-lg font-semibold text-white mb-4">Catatan</h2> |
|
<dl class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-slate-400"> |
|
<div> |
|
<dt class="font-medium text-cyan-400 mb-1">API Token</dt> |
|
<dd>Token statis <code class="text-cyan-300">dbl_...</code> untuk script/n8n. Scope menentukan akses endpoint.</dd> |
|
</div> |
|
<div> |
|
<dt class="font-medium text-violet-400 mb-1">OAuth2 Client</dt> |
|
<dd>Client Credentials untuk integrasi enterprise. Request token ke <code class="text-cyan-300">/oauth/token</code>.</dd> |
|
</div> |
|
<div> |
|
<dt class="font-medium text-amber-400 mb-1">Keamanan</dt> |
|
<dd>Secret/token hanya ditampilkan sekali saat dibuat. Revoke segera jika bocor.</dd> |
|
</div> |
|
<div> |
|
<dt class="font-medium text-emerald-400 mb-1">Dokumentasi</dt> |
|
<dd>Uji API interaktif di <a href="{{ route('api-docs.index') }}" class="text-cyan-400 hover:underline">API Docs</a>.</dd> |
|
</div> |
|
</dl> |
|
</aside> |
|
</div> |
|
@endsection
|
|
|