user(); $credentials = GatewayUserCredential::where('gateway_user_id', $user->user_id) ->orderBy('provider') ->get() ->map(function ($credential) { return [ 'id' => $credential->id, 'provider' => $credential->provider, 'provider_name' => $this->getProviderName($credential->provider), 'api_key_preview' => $this->maskApiKey($credential->api_key), 'organization_id' => $credential->organization_id, 'is_active' => $credential->is_active, 'status' => $credential->test_status ?? 'not_tested', 'last_used' => $credential->last_used_at?->toIso8601String(), 'last_tested' => $credential->last_tested_at?->toIso8601String(), 'test_result' => $credential->last_tested_at ? [ 'status' => $credential->test_status ?? 'unknown', 'message' => $credential->test_error ?: 'Connection successful', 'tested_at' => $credential->last_tested_at->toIso8601String(), ] : null, 'created_at' => $credential->created_at->toIso8601String(), 'updated_at' => $credential->updated_at->toIso8601String(), ]; }); return response()->json([ 'data' => $credentials, ]); } /** * Add new provider credentials * * Create new credentials for a specific provider. Optionally test the * connection before saving. * * ## Request Body * * ```json * { * "provider": "openai", * "api_key": "sk-proj-abc123...", * "organization_id": null, * "test_connection": true * } * ``` * * ## Example Response * * ```json * { * "data": { * "id": 3, * "provider": "openai", * "provider_name": "OpenAI", * "api_key_preview": "sk-proj-...xyz", * "organization_id": null, * "is_active": true, * "status": "verified", * "test_result": { * "status": "success", * "message": "Connection successful", * "model_tested": "gpt-3.5-turbo" * }, * "created_at": "2025-11-19T12:00:00Z" * }, * "message": "Credentials successfully added and verified" * } * ``` * * @tags Credentials * * @param Request $request * @return JsonResponse */ public function store(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'provider' => 'required|string|in:openai,anthropic,gemini,deepseek,mistral', 'api_key' => 'required|string|min:10', 'organization_id' => 'nullable|string', 'test_connection' => 'sometimes|boolean', ]); if ($validator->fails()) { return response()->json([ 'error' => [ 'code' => 'validation_error', 'message' => 'Invalid request data', 'status' => 422, 'details' => $validator->errors(), ], ], 422); } $user = $request->user(); // Check if credentials already exist for this provider $existing = GatewayUserCredential::where('gateway_user_id', $user->user_id) ->where('provider', $request->input('provider')) ->first(); if ($existing) { return response()->json([ 'error' => [ 'code' => 'already_exists', 'message' => "Credentials for provider '{$request->input('provider')}' already exist. Use PUT to update.", 'status' => 409, ], ], 409); } // Create credentials $credential = new GatewayUserCredential(); $credential->gateway_user_id = $user->user_id; $credential->provider = $request->input('provider'); $credential->api_key = $request->input('api_key'); // Will be encrypted by model $credential->organization_id = $request->input('organization_id'); $credential->is_active = true; // Test connection if requested if ($request->input('test_connection', true)) { $testResult = $this->testCredentials($credential); $credential->test_status = $testResult['status']; $credential->test_error = $testResult['error'] ?? null; $credential->last_tested_at = now(); if ($testResult['status'] !== 'success') { return response()->json([ 'error' => [ 'code' => 'test_failed', 'message' => 'Credential test failed', 'status' => 400, 'details' => [ 'test_error' => $testResult['error'], ], ], ], 400); } } $credential->save(); return response()->json([ 'data' => [ 'id' => $credential->id, 'provider' => $credential->provider, 'provider_name' => $this->getProviderName($credential->provider), 'api_key_preview' => $this->maskApiKey($credential->api_key), 'organization_id' => $credential->organization_id, 'is_active' => $credential->is_active, 'status' => $credential->test_status ?? 'not_tested', 'test_result' => $credential->last_tested_at ? [ 'status' => $credential->test_status, 'message' => $credential->test_error ?: 'Connection successful', ] : null, 'created_at' => $credential->created_at->toIso8601String(), ], 'message' => $request->input('test_connection', true) ? 'Credentials successfully added and verified' : 'Credentials successfully added', ], 201); } /** * Update existing credentials * * Update credentials for an existing provider. Can update API key, * organization ID, or active status. * * ## Request Body * * ```json * { * "api_key": "sk-proj-new-key-...", * "organization_id": "org-123", * "is_active": true, * "test_connection": true * } * ``` * * @tags Credentials * * @param Request $request * @param int $id * @return JsonResponse */ public function update(Request $request, int $id): JsonResponse { $validator = Validator::make($request->all(), [ 'api_key' => 'sometimes|string|min:10', 'organization_id' => 'nullable|string', 'is_active' => 'sometimes|boolean', 'test_connection' => 'sometimes|boolean', ]); if ($validator->fails()) { return response()->json([ 'error' => [ 'code' => 'validation_error', 'message' => 'Invalid request data', 'status' => 422, 'details' => $validator->errors(), ], ], 422); } $user = $request->user(); $credential = GatewayUserCredential::where('id', $id) ->where('gateway_user_id', $user->user_id) ->first(); if (!$credential) { return response()->json([ 'error' => [ 'code' => 'not_found', 'message' => 'Credentials not found', 'status' => 404, ], ], 404); } // Update fields if ($request->has('api_key')) { $credential->api_key = $request->input('api_key'); } if ($request->has('organization_id')) { $credential->organization_id = $request->input('organization_id'); } if ($request->has('is_active')) { $credential->is_active = $request->input('is_active'); } // Test connection if requested or if API key changed if ($request->input('test_connection', $request->has('api_key'))) { $testResult = $this->testCredentials($credential); $credential->test_status = $testResult['status']; $credential->test_error = $testResult['error'] ?? null; $credential->last_tested_at = now(); if ($testResult['status'] !== 'success') { return response()->json([ 'error' => [ 'code' => 'test_failed', 'message' => 'Credential test failed', 'status' => 400, 'details' => [ 'test_error' => $testResult['error'], ], ], ], 400); } } $credential->save(); return response()->json([ 'data' => [ 'id' => $credential->id, 'provider' => $credential->provider, 'provider_name' => $this->getProviderName($credential->provider), 'api_key_preview' => $this->maskApiKey($credential->api_key), 'organization_id' => $credential->organization_id, 'is_active' => $credential->is_active, 'status' => $credential->test_status ?? 'not_tested', 'test_result' => $credential->last_tested_at ? [ 'status' => $credential->test_status, 'message' => $credential->test_error ?: 'Connection successful', ] : null, 'updated_at' => $credential->updated_at->toIso8601String(), ], 'message' => 'Credentials successfully updated', ]); } /** * Delete credentials * * Remove credentials for a specific provider. This will prevent any further * requests to this provider. * * @tags Credentials * * @param Request $request * @param int $id * @return JsonResponse */ public function destroy(Request $request, int $id): JsonResponse { $user = $request->user(); $credential = GatewayUserCredential::where('id', $id) ->where('gateway_user_id', $user->user_id) ->first(); if (!$credential) { return response()->json([ 'error' => [ 'code' => 'not_found', 'message' => 'Credentials not found', 'status' => 404, ], ], 404); } $provider = $credential->provider; $credential->delete(); return response()->json([ 'message' => "Credentials for provider '{$provider}' successfully deleted", ]); } /** * Test credentials without saving changes * * Test if credentials are valid by making a test request to the provider. * Does not modify the stored credentials. * * ## Example Response * * ```json * { * "status": "success", * "message": "Connection successful", * "details": { * "provider": "openai", * "model_tested": "gpt-3.5-turbo", * "response_time_ms": 245, * "tested_at": "2025-11-19T12:05:00Z" * } * } * ``` * * @tags Credentials * * @param Request $request * @param int $id * @return JsonResponse */ public function test(Request $request, int $id): JsonResponse { $user = $request->user(); $credential = GatewayUserCredential::where('id', $id) ->where('gateway_user_id', $user->user_id) ->first(); if (!$credential) { return response()->json([ 'error' => [ 'code' => 'not_found', 'message' => 'Credentials not found', 'status' => 404, ], ], 404); } $testResult = $this->testCredentials($credential); // Update test results $credential->test_status = $testResult['status']; $credential->test_error = $testResult['error'] ?? null; $credential->last_tested_at = now(); $credential->save(); if ($testResult['status'] !== 'success') { return response()->json([ 'status' => 'failed', 'message' => 'Connection test failed', 'error' => $testResult['error'], 'tested_at' => now()->toIso8601String(), ], 400); } return response()->json([ 'status' => 'success', 'message' => 'Connection successful', 'details' => [ 'provider' => $credential->provider, 'tested_at' => now()->toIso8601String(), ], ]); } /** * Test credentials by making a simple API call */ private function testCredentials(GatewayUserCredential $credential): array { try { $provider = \App\Services\LLM\ProviderFactory::create( $credential->provider, $credential->api_key ); // Try to get models list as a simple test $models = $provider->getAvailableModels(); if (empty($models)) { return [ 'status' => 'failed', 'error' => 'No models returned from provider', ]; } return [ 'status' => 'success', ]; } catch (\Exception $e) { Log::error('Credential test failed', [ 'provider' => $credential->provider, 'error' => $e->getMessage(), ]); return [ 'status' => 'failed', 'error' => $e->getMessage(), ]; } } /** * Mask API key for display */ private function maskApiKey(string $apiKey): string { $length = strlen($apiKey); if ($length <= 8) { return str_repeat('*', $length); } // Show first 4 and last 4 characters return substr($apiKey, 0, 4) . '...' . substr($apiKey, -4); } /** * Get human-readable provider name */ private function getProviderName(string $provider): string { return match ($provider) { 'openai' => 'OpenAI', 'anthropic' => 'Anthropic', 'gemini' => 'Google Gemini', 'deepseek' => 'DeepSeek', 'mistral' => 'Mistral AI', default => ucfirst($provider), }; } }