diff --git a/app/Http/Controllers/Api/Admin/ClientController.php b/app/Http/Controllers/Api/Admin/ClientController.php index 98a2e44..ababc7b 100644 --- a/app/Http/Controllers/Api/Admin/ClientController.php +++ b/app/Http/Controllers/Api/Admin/ClientController.php @@ -33,32 +33,121 @@ public function __construct( } /** - * 檢查客戶是否有活躍的令牌 + * 獲取客戶列表 */ - private function hasActiveTokens(Client $client): bool + public function index(): JsonResponse { - $pattern = 'auth_token:*'; - $keys = Redis::keys($pattern); + try { + $clients = Client::with('llmProvider:id,name,service_name') + ->select([ + 'id', + 'name', + 'llm_provider_id', + 'rate_limit', + 'timeout', + 'status', + 'created_at', + ]) + ->get(); - foreach ($keys as $key) { - $data = Redis::get($key); - if ($data) { - $tokenData = json_decode($data, true); - if ($tokenData && - $tokenData['client_id'] === $client->id && - now()->isBefore($tokenData['expires_at'])) { - return true; - } - } + return $this->success($clients); + + } catch (\Exception $e) { + Log::error('Error fetching clients', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return $this->error( + ErrorCode::SERVER_ERROR, + ErrorCode::getMessage(ErrorCode::SERVER_ERROR) + ); } - - return false; } /** - * 停用客戶 + * 新增客戶 */ - public function deactivate(int $id): JsonResponse + public function store(Request $request): JsonResponse + { + try { + $validated = $request->validate([ + 'name' => [ + 'required', + 'string', + 'max:100', + 'unique:clients', + ], + 'llm_provider_id' => [ + 'required', + 'integer', + 'exists:llm_providers,id', + ], + 'rate_limit' => [ + 'required', + 'integer', + 'min:1', + 'max:1000', + ], + 'timeout' => [ + 'required', + 'integer', + 'min:1', + 'max:300', + ], + ]); + + $validated['status'] = Client::STATUS_ACTIVE; + $client = Client::create($validated); + + // 加載 LLM 提供商信息 + $client->load('llmProvider:id,name,service_name'); + + $this->logService->logOperation( + 'admin', + $this->admin->id, + "Created client: {$client->name}" + ); + + return $this->created([ + 'id' => $client->id, + 'name' => $client->name, + 'llm_provider_id' => $client->llm_provider_id, + 'llm_provider' => [ + 'id' => $client->llmProvider->id, + 'name' => $client->llmProvider->name, + 'service_name' => $client->llmProvider->service_name, + ], + 'rate_limit' => $client->rate_limit, + 'timeout' => $client->timeout, + 'status' => $client->status, + 'created_at' => $client->created_at->toIso8601String(), + ]); + + } catch (ValidationException $e) { + return $this->error( + ErrorCode::VALIDATION_ERROR, + ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR), + $e->errors() + ); + } catch (\Exception $e) { + Log::error('Error creating client', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'request_data' => $request->all(), + ]); + + return $this->error( + ErrorCode::SERVER_ERROR, + ErrorCode::getMessage(ErrorCode::SERVER_ERROR) + ); + } + } + + /** + * 修改客戶 + */ + public function update(Request $request, int $id): JsonResponse { try { $client = Client::findOrFail($id); @@ -70,32 +159,91 @@ public function deactivate(int $id): JsonResponse ); } - if ($this->hasActiveTokens($client)) { - return $this->error( - ErrorCode::CLIENT_HAS_ACTIVE_TOKENS, - '該客戶有活躍的認證令牌,無法停用。' - ); + $validated = $request->validate([ + 'name' => [ + 'required', + 'string', + 'max:100', + "unique:clients,name,{$id}", + ], + 'llm_provider_id' => [ + 'required', + 'integer', + 'exists:llm_providers,id', + ], + 'rate_limit' => [ + 'required', + 'integer', + 'min:1', + 'max:1000', + ], + 'timeout' => [ + 'required', + 'integer', + 'min:1', + 'max:300', + ], + 'status' => [ + 'required', + 'string', + 'in:active,inactive', + ], + ]); + + // 如果要停用客戶,檢查是否有活躍的令牌 + if ($validated['status'] === Client::STATUS_INACTIVE && + $client->status === Client::STATUS_ACTIVE) { + if ($this->hasActiveTokens($client)) { + return $this->error( + ErrorCode::CLIENT_HAS_ACTIVE_TOKENS, + '該客戶有活躍的認證令牌,無法停用。' + ); + } } - $client->update(['status' => Client::STATUS_INACTIVE]); + $client->update($validated); + + // 加載 LLM 提供商信息 + $client->load('llmProvider:id,name,service_name'); $this->logService->logOperation( 'admin', $this->admin->id, - "Deactivated client: {$client->name}" + "Updated client: {$client->name}" ); - return $this->success(); + return $this->success([ + 'id' => $client->id, + 'name' => $client->name, + 'llm_provider_id' => $client->llm_provider_id, + 'llm_provider' => [ + 'id' => $client->llmProvider->id, + 'name' => $client->llmProvider->name, + 'service_name' => $client->llmProvider->service_name, + ], + 'rate_limit' => $client->rate_limit, + 'timeout' => $client->timeout, + 'status' => $client->status, + 'updated_at' => $client->updated_at->toIso8601String(), + ]); + } catch (ValidationException $e) { + return $this->error( + ErrorCode::VALIDATION_ERROR, + ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR), + $e->errors() + ); } catch (ModelNotFoundException $e) { return $this->error( ErrorCode::CLIENT_NOT_FOUND, ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND) ); } catch (\Exception $e) { - Log::error('Error deactivating client', [ + Log::error('Error updating client', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), + 'client_id' => $id, + 'request_data' => $request->all(), ]); return $this->error( @@ -135,7 +283,7 @@ public function destroy(int $id): JsonResponse "Deleted client: {$client->name}" ); - return $this->success(); + return $this->success(null, '客戶已刪除。'); } catch (ModelNotFoundException $e) { return $this->error( @@ -146,6 +294,7 @@ public function destroy(int $id): JsonResponse Log::error('Error deleting client', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), + 'client_id' => $id, ]); return $this->error( @@ -177,7 +326,7 @@ public function generateAuthToken(int $id): JsonResponse ); } - $token = $this->tokenService->generateAuthToken($client); + $result = $this->tokenService->generateAuthToken($client); $this->logService->logOperation( 'admin', @@ -185,15 +334,7 @@ public function generateAuthToken(int $id): JsonResponse "Generated auth token for client: {$client->name}" ); - return $this->success([ - 'auth_token' => $token['token'], - 'expires_at' => $token['expires_at'], - 'client' => [ - 'id' => $client->id, - 'name' => $client->name, - 'status' => $client->status, - ], - ]); + return $this->success($result); } catch (ModelNotFoundException $e) { return $this->error( @@ -204,6 +345,7 @@ public function generateAuthToken(int $id): JsonResponse Log::error('Error generating auth token', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), + 'client_id' => $id, ]); return $this->error( @@ -212,4 +354,27 @@ public function generateAuthToken(int $id): JsonResponse ); } } + + /** + * 檢查客戶是否有活躍的令牌 + */ + private function hasActiveTokens(Client $client): bool + { + $pattern = 'auth_token:*'; + $keys = Redis::keys($pattern); + + foreach ($keys as $key) { + $data = Redis::get($key); + if ($data) { + $tokenData = json_decode($data, true); + if ($tokenData && + $tokenData['client_id'] === $client->id && + now()->isBefore($tokenData['expires_at'])) { + return true; + } + } + } + + return false; + } } diff --git a/app/Http/Controllers/Api/Admin/LlmProviderController.php b/app/Http/Controllers/Api/Admin/LlmProviderController.php index 3b38bd2..583fa25 100644 --- a/app/Http/Controllers/Api/Admin/LlmProviderController.php +++ b/app/Http/Controllers/Api/Admin/LlmProviderController.php @@ -6,6 +6,7 @@ use App\Constants\ErrorCode; use App\Http\Controllers\Controller; +use App\Models\Admin; use App\Models\LlmProvider; use App\Services\LogService; use App\Traits\ApiResponse; @@ -19,48 +20,31 @@ class LlmProviderController extends Controller { use ApiResponse; + private Admin $admin; + public function __construct( private readonly LogService $logService, - ) {} + Request $request + ) { + $this->admin = $request->admin; + } /** - * 获取 LLM 提供商列表 - * - * @param Request $request - * @return JsonResponse + * 獲取 LLM 提供商列表 */ - public function index(Request $request): JsonResponse + public function index(): JsonResponse { try { - $perPage = $request->input('per_page', 15); - $perPage = min(max($perPage, 1), 100); - $search = $request->input('search'); - $status = $request->input('status'); + $providers = LlmProvider::select([ + 'id', + 'name', + 'service_name', + 'api_url', + 'status', + 'created_at', + ])->get(); - $query = LlmProvider::query()->orderBy('created_at', 'desc'); - - if ($search) { - $query->where(function ($q) use ($search) { - $q->where('name', 'like', "%{$search}%") - ->orWhere('service_name', 'like', "%{$search}%"); - }); - } - - if ($status && in_array($status, ['active', 'inactive'])) { - $query->where('status', $status); - } - - $providers = $query->paginate($perPage); - - return $this->paginate( - $providers->items(), - [ - 'current_page' => $providers->currentPage(), - 'per_page' => $providers->perPage(), - 'total' => $providers->total(), - 'total_pages' => $providers->lastPage(), - ] - ); + return $this->success($providers); } catch (\Exception $e) { Log::error('Error fetching LLM providers', [ @@ -76,10 +60,7 @@ public function index(Request $request): JsonResponse } /** - * 创建 LLM 提供商 - * - * @param Request $request - * @return JsonResponse + * 新增 LLM 提供商 */ public function store(Request $request): JsonResponse { @@ -90,38 +71,42 @@ public function store(Request $request): JsonResponse 'string', 'max:100', 'unique:llm_providers', - 'regex:/^[\w\-\s]+$/u', ], 'service_name' => [ 'required', 'string', 'max:100', - 'regex:/^[\w\-]+$/u', ], 'api_url' => [ 'required', 'string', - 'max:255', 'url', - 'starts_with:https', + 'max:255', + ], + 'api_token' => [ + 'required', + 'string', + 'max:255', ], - 'api_token' => 'required|string|max:255', - ], [ - 'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空格。', - 'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。', - 'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。', ]); - $validated['status'] = 'active'; + $validated['status'] = LlmProvider::STATUS_ACTIVE; $provider = LlmProvider::create($validated); $this->logService->logOperation( 'admin', - $request->admin->id, + $this->admin->id, "Created LLM provider: {$provider->name}" ); - return $this->created($provider); + return $this->created([ + 'id' => $provider->id, + 'name' => $provider->name, + 'service_name' => $provider->service_name, + 'api_url' => $provider->api_url, + 'api_token' => $provider->api_token, + 'created_at' => $provider->created_at->toIso8601String(), + ]); } catch (ValidationException $e) { return $this->error( @@ -144,57 +129,57 @@ public function store(Request $request): JsonResponse } /** - * 更新 LLM 提供商 - * - * @param Request $request - * @param int $id - * @return JsonResponse + * 修改 LLM 提供商 */ public function update(Request $request, int $id): JsonResponse { try { $provider = LlmProvider::findOrFail($id); + if (!$this->admin->canManageLlmProvider($provider->id)) { + return $this->error( + ErrorCode::FORBIDDEN, + '您無權管理該 LLM 提供商。' + ); + } + $validated = $request->validate([ 'name' => [ 'required', 'string', 'max:100', "unique:llm_providers,name,{$id}", - 'regex:/^[\w\-\s]+$/u', ], 'service_name' => [ 'required', 'string', 'max:100', - 'regex:/^[\w\-]+$/u', ], 'api_url' => [ 'required', 'string', - 'max:255', 'url', - 'starts_with:https', + 'max:255', + ], + 'api_token' => [ + 'required', + 'string', + 'max:255', ], - 'api_token' => 'required|string|max:255', 'status' => [ 'required', 'string', 'in:active,inactive', ], - ], [ - 'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空格。', - 'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。', - 'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。', - 'status.in' => '状态只能是 active 或 inactive。', ]); - // 如果状态改为 inactive,检查是否有客户正在使用 - if ($validated['status'] === 'inactive' && $provider->status === 'active') { + // 如果要停用提供商,檢查是否有客戶在使用 + if ($validated['status'] === LlmProvider::STATUS_INACTIVE && + $provider->status === LlmProvider::STATUS_ACTIVE) { if ($provider->clients()->exists()) { return $this->error( ErrorCode::RESOURCE_IN_USE, - '该提供商正在被客户使用,无法停用。' + '該提供商正在被客戶使用,無法停用。' ); } } @@ -203,11 +188,18 @@ public function update(Request $request, int $id): JsonResponse $this->logService->logOperation( 'admin', - $request->admin->id, + $this->admin->id, "Updated LLM provider: {$provider->name}" ); - return $this->success($provider); + return $this->success([ + 'id' => $provider->id, + 'name' => $provider->name, + 'service_name' => $provider->service_name, + 'api_url' => $provider->api_url, + 'api_token' => $provider->api_token, + 'updated_at' => $provider->updated_at->toIso8601String(), + ]); } catch (ValidationException $e) { return $this->error( @@ -236,22 +228,25 @@ public function update(Request $request, int $id): JsonResponse } /** - * 删除 LLM 提供商 - * - * @param Request $request - * @param int $id - * @return JsonResponse + * 刪除 LLM 提供商 */ - public function destroy(Request $request, int $id): JsonResponse + public function destroy(int $id): JsonResponse { try { $provider = LlmProvider::findOrFail($id); - // 检查是否有客户正在使用 + if (!$this->admin->canManageLlmProvider($provider->id)) { + return $this->error( + ErrorCode::FORBIDDEN, + '您無權管理該 LLM 提供商。' + ); + } + + // 檢查是否有客戶在使用 if ($provider->clients()->exists()) { return $this->error( ErrorCode::RESOURCE_IN_USE, - '该提供商正在被客户使用,无法删除。' + '該提供商正在被客戶使用,無法刪除。' ); } @@ -260,11 +255,11 @@ public function destroy(Request $request, int $id): JsonResponse $this->logService->logOperation( 'admin', - $request->admin->id, + $this->admin->id, "Deleted LLM provider: {$providerName}" ); - return $this->success(null, '提供商已删除。'); + return $this->success(null, 'LLM 提供商已刪除。'); } catch (ModelNotFoundException $e) { return $this->error( diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index 8ec9387..dacb02b 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -49,11 +49,7 @@ public function getAccessToken(Request $request): JsonResponse $result = $this->tokenService->generateAccessToken($authTokenData); - return $this->success([ - 'access_token' => $result['access_token'], - 'expires_in' => $result['expires_in'], - 'token_type' => $result['token_type'], - ]); + return $this->success($result); } catch (ValidationException $e) { return $this->error( diff --git a/app/Http/Controllers/Api/LlmController.php b/app/Http/Controllers/Api/LlmController.php index 9feec6d..8275aba 100644 --- a/app/Http/Controllers/Api/LlmController.php +++ b/app/Http/Controllers/Api/LlmController.php @@ -22,10 +22,7 @@ public function __construct( ) {} /** - * 发送 LLM 请求 - * - * @param Request $request - * @return JsonResponse + * 發送 LLM 請求 */ public function request(Request $request): JsonResponse { @@ -47,7 +44,6 @@ public function request(Request $request): JsonResponse return $this->success([ 'response' => $result['response'], - 'usage' => $result['usage'], ]); } catch (ValidationException $e) { diff --git a/app/Services/Auth/TokenService.php b/app/Services/Auth/TokenService.php index 0e15793..10b7461 100644 --- a/app/Services/Auth/TokenService.php +++ b/app/Services/Auth/TokenService.php @@ -24,7 +24,7 @@ public function __construct( /** * 生成認證令牌 * - * @return array{token: string, expires_at: string} + * @return array{auth_token: string, expires_at: string, client_id: int} */ public function generateAuthToken(Client $client, ?int $expiresInHours = null): array { @@ -56,8 +56,9 @@ public function generateAuthToken(Client $client, ?int $expiresInHours = null): ); return [ - 'token' => $token, + 'auth_token' => $token, 'expires_at' => $expiresAt->toIso8601String(), + 'client_id' => $client->id, ]; } @@ -91,7 +92,7 @@ public function validateAuthToken(string $token): ?array /** * 生成訪問令牌 * - * @return array{access_token: string, expires_in: int, token_type: string} + * @return array{access_token: string, expires_in: int} */ public function generateAccessToken(array $authTokenData): array { @@ -118,7 +119,6 @@ public function generateAccessToken(array $authTokenData): array return [ 'access_token' => $accessToken, 'expires_in' => self::ACCESS_TOKEN_TTL, - 'token_type' => 'Bearer', ]; }