205 lines
7.4 KiB
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();
|
|
}
|
|
}
|