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 $values * @return array */ private function normalizeStringArray(array $values): array { return collect($values) ->map(fn ($value): string => trim((string) $value)) ->filter() ->unique() ->values() ->all(); } /** * @param array $remap * @return array */ 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 $validated * @return array */ 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(); } }