feat(api): add entitlements endpoint
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
db1efd502c
commit
797c5f9571
3 changed files with 168 additions and 0 deletions
101
src/php/src/Api/Controllers/Api/EntitlementApiController.php
Normal file
101
src/php/src/Api/Controllers/Api/EntitlementApiController.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Api\Controllers\Api;
|
||||
|
||||
use Core\Api\Concerns\HasApiResponses;
|
||||
use Core\Api\Concerns\ResolvesWorkspace;
|
||||
use Core\Api\Models\ApiKey;
|
||||
use Core\Api\Services\ApiUsageService;
|
||||
use Core\Front\Controller;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Entitlements API controller.
|
||||
*
|
||||
* Returns the current workspace's plan limits and usage snapshot.
|
||||
*/
|
||||
class EntitlementApiController extends Controller
|
||||
{
|
||||
use HasApiResponses;
|
||||
use ResolvesWorkspace;
|
||||
|
||||
public function __construct(
|
||||
protected ApiUsageService $usageService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the current workspace entitlements.
|
||||
*
|
||||
* GET /api/entitlements
|
||||
*/
|
||||
public function show(Request $request): JsonResponse
|
||||
{
|
||||
$workspace = $this->resolveWorkspace($request);
|
||||
|
||||
if (! $workspace instanceof Workspace) {
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$apiKey = $request->attributes->get('api_key');
|
||||
$authType = $request->attributes->get('auth_type', 'session');
|
||||
$rateLimitProfile = $this->resolveRateLimitProfile($authType);
|
||||
$activeApiKeys = ApiKey::query()
|
||||
->forWorkspace($workspace->id)
|
||||
->active()
|
||||
->count();
|
||||
|
||||
$usage = $this->usageService->getWorkspaceSummary($workspace->id);
|
||||
|
||||
return response()->json([
|
||||
'workspace_id' => $workspace->id,
|
||||
'workspace' => [
|
||||
'id' => $workspace->id,
|
||||
'name' => $workspace->name ?? null,
|
||||
],
|
||||
'authentication' => [
|
||||
'type' => $authType,
|
||||
'scopes' => $apiKey instanceof ApiKey ? $apiKey->scopes : null,
|
||||
],
|
||||
'limits' => [
|
||||
'rate_limit' => $rateLimitProfile,
|
||||
'api_keys' => [
|
||||
'active' => $activeApiKeys,
|
||||
'maximum' => (int) config('api.keys.max_per_workspace', 10),
|
||||
'remaining' => max(0, (int) config('api.keys.max_per_workspace', 10) - $activeApiKeys),
|
||||
],
|
||||
'webhooks' => [
|
||||
'maximum' => (int) config('api.webhooks.max_per_workspace', 5),
|
||||
],
|
||||
],
|
||||
'usage' => $usage,
|
||||
'features' => [
|
||||
'pixel' => true,
|
||||
'mcp' => true,
|
||||
'webhooks' => true,
|
||||
'usage_alerts' => (bool) config('api.alerts.enabled', true),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the rate limit profile for the current auth context.
|
||||
*/
|
||||
protected function resolveRateLimitProfile(string $authType): array
|
||||
{
|
||||
$rateLimits = (array) config('api.rate_limits', []);
|
||||
$key = $authType === 'session' ? 'default' : 'authenticated';
|
||||
$profile = (array) ($rateLimits[$key] ?? []);
|
||||
|
||||
return [
|
||||
'name' => $key,
|
||||
'limit' => (int) ($profile['limit'] ?? 0),
|
||||
'window' => (int) ($profile['window'] ?? 60),
|
||||
'burst' => (float) ($profile['burst'] ?? 1.0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Core\Api\Controllers\Api\UnifiedPixelController;
|
||||
use Core\Api\Controllers\Api\EntitlementApiController;
|
||||
use Core\Api\Controllers\McpApiController;
|
||||
use Core\Api\Middleware\PublicApiCors;
|
||||
use Core\Mcp\Middleware\McpApiKeyAuth;
|
||||
|
|
@ -32,6 +33,18 @@ Route::middleware([PublicApiCors::class, 'api.rate'])
|
|||
->name('track');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Entitlements (authenticated)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Route::middleware(['auth.api', 'api.scope.enforce'])
|
||||
->prefix('entitlements')
|
||||
->name('api.entitlements.')
|
||||
->group(function () {
|
||||
Route::get('/', [EntitlementApiController::class, 'show'])
|
||||
->name('show');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MCP HTTP Bridge (API key auth)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
54
src/php/src/Api/Tests/Feature/EntitlementsEndpointTest.php
Normal file
54
src/php/src/Api/Tests/Feature/EntitlementsEndpointTest.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mod\Api\Models\ApiKey;
|
||||
use Mod\Api\Services\ApiUsageService;
|
||||
use Mod\Tenant\Models\User;
|
||||
use Mod\Tenant\Models\Workspace;
|
||||
|
||||
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->workspace = Workspace::factory()->create();
|
||||
$this->workspace->users()->attach($this->user->id, [
|
||||
'role' => 'owner',
|
||||
'is_default' => true,
|
||||
]);
|
||||
|
||||
$result = ApiKey::generate(
|
||||
$this->workspace->id,
|
||||
$this->user->id,
|
||||
'Entitlements Key',
|
||||
[ApiKey::SCOPE_READ]
|
||||
);
|
||||
|
||||
$this->plainKey = $result['plain_key'];
|
||||
$this->apiKey = $result['api_key'];
|
||||
});
|
||||
|
||||
it('returns entitlement limits and usage for the current workspace', function () {
|
||||
app(ApiUsageService::class)->record(
|
||||
apiKeyId: $this->apiKey->id,
|
||||
workspaceId: $this->workspace->id,
|
||||
endpoint: '/api/entitlements',
|
||||
method: 'GET',
|
||||
statusCode: 200,
|
||||
responseTimeMs: 42,
|
||||
ipAddress: '127.0.0.1',
|
||||
userAgent: 'Pest'
|
||||
);
|
||||
|
||||
$response = $this->getJson('/api/entitlements', [
|
||||
'Authorization' => "Bearer {$this->plainKey}",
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('workspace_id', $this->workspace->id);
|
||||
$response->assertJsonPath('authentication.type', 'api_key');
|
||||
$response->assertJsonPath('limits.api_keys.maximum', config('api.keys.max_per_workspace'));
|
||||
$response->assertJsonPath('limits.api_keys.active', 1);
|
||||
$response->assertJsonPath('usage.totals.requests', 1);
|
||||
$response->assertJsonPath('features.mcp', true);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue