BastionSSO/app/Http/Controllers/Api/OauthClientController.php

205 lines
7.4 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreOauthClientRequest;
use App\Http\Requests\UpdateOauthClientRequest;
use App\Models\OauthClient;
use App\Models\OauthScope;
use hg\apidoc\annotation as Apidoc;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
#[Apidoc\Title('OAuth 客户端管理')]
class OauthClientController extends Controller
{
public function __construct()
{
$this->middleware('auth:api');
$this->middleware('permission:platform.oauth_clients.view,api')->only(['index', 'show']);
$this->middleware('permission:platform.oauth_clients.manage,api')->only(['store', 'update', 'destroy', 'resetSecret']);
}
#[Apidoc\Title('OAuth 客户端列表'), Apidoc\Method('GET'), Apidoc\Url('/oauth/clients')]
public function index(Request $request): JsonResponse
{
$validated = $request->validate([
'per_page' => ['nullable', 'integer', 'min:1', 'max:200'],
]);
$perPage = (int) ($validated['per_page'] ?? 20);
$paginator = OauthClient::query()
->with('scopes')
->latest()
->paginate($perPage);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $paginator]);
}
#[Apidoc\Title('创建 OAuth 客户端'), Apidoc\Method('POST'), Apidoc\Url('/oauth/clients')]
public function store(StoreOauthClientRequest $request): JsonResponse
{
$validated = $request->validated();
$plainSecret = $this->generateRandomToken(40);
$client = OauthClient::query()->create([
'name' => trim((string) $validated['name']),
'logo_url' => $validated['logo_url'] ?? null,
'client_id' => $this->generateClientId(),
'client_secret_hash' => Hash::make($plainSecret),
'redirect_uris' => $this->normalizeStringArray($validated['redirect_uris'] ?? []),
'allowed_userinfo_fields' => $this->normalizeStringArray($validated['allowed_userinfo_fields'] ?? []),
'userinfo_claim_remap' => $this->normalizeRemap($validated['userinfo_claim_remap'] ?? []),
'is_confidential' => true,
'is_active' => (bool) ($validated['is_active'] ?? true),
]);
$client->scopes()->sync($this->resolveScopeIds($validated));
$this->auditLog($request, 'oauth_client_create', ['metadata' => ['oauth_client_id' => $client->id]]);
return response()->json([
'code' => 0,
'message' => 'ok',
'data' => [
'client' => $client->fresh('scopes'),
'client_secret' => $plainSecret,
],
], 201);
}
#[Apidoc\Title('OAuth 客户端详情'), Apidoc\Method('GET'), Apidoc\Url('/oauth/clients/{id}')]
public function show(int $id): JsonResponse
{
$client = OauthClient::query()->with('scopes')->findOrFail($id);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $client]);
}
#[Apidoc\Title('更新 OAuth 客户端'), Apidoc\Method('PUT'), Apidoc\Url('/oauth/clients/{id}')]
public function update(UpdateOauthClientRequest $request, int $id): JsonResponse
{
$client = OauthClient::query()->findOrFail($id);
$validated = $request->validated();
$client->update([
'name' => trim((string) $validated['name']),
'logo_url' => $validated['logo_url'] ?? null,
'redirect_uris' => $this->normalizeStringArray($validated['redirect_uris'] ?? []),
'allowed_userinfo_fields' => $this->normalizeStringArray($validated['allowed_userinfo_fields'] ?? []),
'userinfo_claim_remap' => $this->normalizeRemap($validated['userinfo_claim_remap'] ?? []),
'is_confidential' => true,
'is_active' => (bool) ($validated['is_active'] ?? true),
]);
$client->scopes()->sync($this->resolveScopeIds($validated, $client));
$this->auditLog($request, 'oauth_client_update', ['metadata' => ['oauth_client_id' => $client->id]]);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $client->fresh('scopes')]);
}
#[Apidoc\Title('删除 OAuth 客户端'), Apidoc\Method('DELETE'), Apidoc\Url('/oauth/clients/{id}')]
public function destroy(Request $request, int $id): JsonResponse
{
$client = OauthClient::query()->findOrFail($id);
$this->auditLog($request, 'oauth_client_delete', ['metadata' => ['oauth_client_id' => $client->id]]);
$client->delete();
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
}
#[Apidoc\Title('重置 OAuth 客户端密钥'), Apidoc\Method('POST'), Apidoc\Url('/oauth/clients/{id}/reset-secret')]
public function resetSecret(Request $request, int $id): JsonResponse
{
$client = OauthClient::query()->findOrFail($id);
$plainSecret = $this->generateRandomToken(40);
$client->update([
'client_secret_hash' => Hash::make($plainSecret),
'is_confidential' => true,
]);
$this->auditLog($request, 'oauth_client_reset_secret', ['metadata' => ['oauth_client_id' => $client->id]]);
return response()->json([
'code' => 0,
'message' => 'ok',
'data' => [
'client_id' => $client->client_id,
'client_secret' => $plainSecret,
],
]);
}
/**
* @param array<int, mixed> $values
* @return array<int, string>
*/
private function normalizeStringArray(array $values): array
{
return collect($values)
->map(fn ($value): string => trim((string) $value))
->filter()
->unique()
->values()
->all();
}
/**
* @param array<string, mixed> $remap
* @return array<string, string>
*/
private function normalizeRemap(array $remap): array
{
$result = [];
foreach ($remap as $source => $target) {
$from = trim((string) $source);
$to = trim((string) $target);
if ($from === '' || $to === '' || $from === 'sub') {
continue;
}
$result[$from] = $to;
}
return $result;
}
private function generateClientId(): string
{
do {
$clientId = 'cli_'.strtolower($this->generateRandomToken(18));
} while (OauthClient::query()->where('client_id', $clientId)->exists());
return $clientId;
}
private function generateRandomToken(int $bytes): string
{
return rtrim(strtr(base64_encode(random_bytes($bytes)), '+/', '-_'), '=');
}
/**
* @param array<string, mixed> $validated
* @return array<int, int>
*/
private function resolveScopeIds(array $validated, ?OauthClient $client = null): array
{
if (array_key_exists('scope_ids', $validated)) {
return collect($validated['scope_ids'])
->map(fn ($scopeId): int => (int) $scopeId)
->values()
->all();
}
if ($client !== null) {
return $client->scopes()->pluck('oauth_scopes.id')->map(fn ($scopeId): int => (int) $scopeId)->all();
}
return OauthScope::query()
->where('is_active', true)
->pluck('id')
->map(fn ($scopeId): int => (int) $scopeId)
->all();
}
}