|
| 1 | +// Copyright (c) 2025 Nikolaos Protopapas |
| 2 | +// Licensed under the MIT License |
| 3 | + |
| 4 | +using Microsoft.AspNetCore.Mvc; |
| 5 | +using LocalizationManager.Core.Configuration; |
| 6 | +using LocalizationManager.Core.Translation; |
| 7 | +using LocalizationManager.Models.Api; |
| 8 | + |
| 9 | +namespace LocalizationManager.Controllers; |
| 10 | + |
| 11 | +[ApiController] |
| 12 | +[Route("api/[controller]")] |
| 13 | +public class CredentialsController : ControllerBase |
| 14 | +{ |
| 15 | + private readonly ConfigurationService _configService; |
| 16 | + private readonly string _resourcePath; |
| 17 | + |
| 18 | + public CredentialsController(IConfiguration configuration, ConfigurationService configService) |
| 19 | + { |
| 20 | + _configService = configService; |
| 21 | + _resourcePath = configuration["ResourcePath"] ?? Directory.GetCurrentDirectory(); |
| 22 | + } |
| 23 | + |
| 24 | + /// <summary> |
| 25 | + /// Get list of all providers with their credential source status |
| 26 | + /// </summary> |
| 27 | + [HttpGet("providers")] |
| 28 | + public ActionResult<CredentialProvidersResponse> GetProviders() |
| 29 | + { |
| 30 | + try |
| 31 | + { |
| 32 | + var config = _configService.GetConfiguration(); |
| 33 | + var providerInfos = TranslationProviderFactory.GetProviderInfos(); |
| 34 | + var secureProviders = SecureCredentialManager.GetConfiguredProviders(); |
| 35 | + |
| 36 | + var providers = providerInfos.Select(p => |
| 37 | + { |
| 38 | + string? source = null; |
| 39 | + |
| 40 | + // Check environment variable first (highest priority) |
| 41 | + var envVar = $"LRM_{p.Name.ToUpperInvariant()}_API_KEY"; |
| 42 | + if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(envVar))) |
| 43 | + { |
| 44 | + source = "environment"; |
| 45 | + } |
| 46 | + // Check secure store |
| 47 | + else if (secureProviders.Contains(p.Name, StringComparer.OrdinalIgnoreCase)) |
| 48 | + { |
| 49 | + source = "secure_store"; |
| 50 | + } |
| 51 | + // Check config file |
| 52 | + else if (!string.IsNullOrWhiteSpace(config?.Translation?.ApiKeys?.GetKeyForProvider(p.Name))) |
| 53 | + { |
| 54 | + source = "config_file"; |
| 55 | + } |
| 56 | + |
| 57 | + return new CredentialProviderInfo |
| 58 | + { |
| 59 | + Provider = p.Name, |
| 60 | + DisplayName = p.DisplayName, |
| 61 | + RequiresApiKey = p.RequiresApiKey, |
| 62 | + Source = source, |
| 63 | + IsConfigured = source != null || !p.RequiresApiKey |
| 64 | + }; |
| 65 | + }).ToList(); |
| 66 | + |
| 67 | + return Ok(new CredentialProvidersResponse |
| 68 | + { |
| 69 | + Providers = providers, |
| 70 | + UseSecureCredentialStore = config?.Translation?.UseSecureCredentialStore ?? false |
| 71 | + }); |
| 72 | + } |
| 73 | + catch (Exception) |
| 74 | + { |
| 75 | + return StatusCode(500, new ErrorResponse { Error = "An error occurred while processing your request" }); |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + /// <summary> |
| 80 | + /// Set API key in secure credential store |
| 81 | + /// </summary> |
| 82 | + [HttpPut("{provider}")] |
| 83 | + public ActionResult<OperationResponse> SetApiKey(string provider, [FromBody] SetApiKeyRequest request) |
| 84 | + { |
| 85 | + try |
| 86 | + { |
| 87 | + if (string.IsNullOrWhiteSpace(provider)) |
| 88 | + { |
| 89 | + return BadRequest(new ErrorResponse { Error = "Provider name is required" }); |
| 90 | + } |
| 91 | + |
| 92 | + if (string.IsNullOrWhiteSpace(request?.ApiKey)) |
| 93 | + { |
| 94 | + return BadRequest(new ErrorResponse { Error = "API key is required" }); |
| 95 | + } |
| 96 | + |
| 97 | + // Validate provider name |
| 98 | + var validProviders = TranslationProviderFactory.GetProviderInfos() |
| 99 | + .Where(p => p.RequiresApiKey) |
| 100 | + .Select(p => p.Name.ToLowerInvariant()) |
| 101 | + .ToHashSet(); |
| 102 | + |
| 103 | + if (!validProviders.Contains(provider.ToLowerInvariant())) |
| 104 | + { |
| 105 | + return BadRequest(new ErrorResponse { Error = $"Unknown or keyless provider: {provider}" }); |
| 106 | + } |
| 107 | + |
| 108 | + // Store in secure credential store |
| 109 | + SecureCredentialManager.SetApiKey(provider.ToLowerInvariant(), request.ApiKey); |
| 110 | + |
| 111 | + // Enable secure credential store in config if not already |
| 112 | + var config = _configService.GetConfiguration() ?? new ConfigurationModel(); |
| 113 | + if (config.Translation == null) |
| 114 | + { |
| 115 | + config.Translation = new TranslationConfiguration(); |
| 116 | + } |
| 117 | + if (!config.Translation.UseSecureCredentialStore) |
| 118 | + { |
| 119 | + config.Translation.UseSecureCredentialStore = true; |
| 120 | + _configService.SaveConfiguration(config); |
| 121 | + } |
| 122 | + |
| 123 | + return Ok(new OperationResponse |
| 124 | + { |
| 125 | + Success = true, |
| 126 | + Message = $"API key for {provider} stored securely" |
| 127 | + }); |
| 128 | + } |
| 129 | + catch (Exception) |
| 130 | + { |
| 131 | + return StatusCode(500, new ErrorResponse { Error = "An error occurred while processing your request" }); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + /// <summary> |
| 136 | + /// Delete API key from secure credential store |
| 137 | + /// </summary> |
| 138 | + [HttpDelete("{provider}")] |
| 139 | + public ActionResult<OperationResponse> DeleteApiKey(string provider) |
| 140 | + { |
| 141 | + try |
| 142 | + { |
| 143 | + if (string.IsNullOrWhiteSpace(provider)) |
| 144 | + { |
| 145 | + return BadRequest(new ErrorResponse { Error = "Provider name is required" }); |
| 146 | + } |
| 147 | + |
| 148 | + var deleted = SecureCredentialManager.DeleteApiKey(provider.ToLowerInvariant()); |
| 149 | + |
| 150 | + if (deleted) |
| 151 | + { |
| 152 | + return Ok(new OperationResponse |
| 153 | + { |
| 154 | + Success = true, |
| 155 | + Message = $"API key for {provider} removed from secure store" |
| 156 | + }); |
| 157 | + } |
| 158 | + else |
| 159 | + { |
| 160 | + return Ok(new OperationResponse |
| 161 | + { |
| 162 | + Success = false, |
| 163 | + Message = $"No API key found for {provider} in secure store" |
| 164 | + }); |
| 165 | + } |
| 166 | + } |
| 167 | + catch (Exception) |
| 168 | + { |
| 169 | + return StatusCode(500, new ErrorResponse { Error = "An error occurred while processing your request" }); |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + /// <summary> |
| 174 | + /// Get the source of an API key (for display purposes, never returns the actual key) |
| 175 | + /// </summary> |
| 176 | + [HttpGet("{provider}/source")] |
| 177 | + public ActionResult<CredentialSourceResponse> GetApiKeySource(string provider) |
| 178 | + { |
| 179 | + try |
| 180 | + { |
| 181 | + if (string.IsNullOrWhiteSpace(provider)) |
| 182 | + { |
| 183 | + return BadRequest(new ErrorResponse { Error = "Provider name is required" }); |
| 184 | + } |
| 185 | + |
| 186 | + var config = _configService.GetConfiguration(); |
| 187 | + var source = ApiKeyResolver.GetApiKeySource(provider.ToLowerInvariant(), config); |
| 188 | + |
| 189 | + return Ok(new CredentialSourceResponse |
| 190 | + { |
| 191 | + Provider = provider, |
| 192 | + Source = source, |
| 193 | + IsConfigured = source != null |
| 194 | + }); |
| 195 | + } |
| 196 | + catch (Exception) |
| 197 | + { |
| 198 | + return StatusCode(500, new ErrorResponse { Error = "An error occurred while processing your request" }); |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + /// <summary> |
| 203 | + /// Test provider connection with configured credentials |
| 204 | + /// </summary> |
| 205 | + [HttpPost("{provider}/test")] |
| 206 | + public async Task<ActionResult<ProviderTestResponse>> TestProvider(string provider) |
| 207 | + { |
| 208 | + try |
| 209 | + { |
| 210 | + if (string.IsNullOrWhiteSpace(provider)) |
| 211 | + { |
| 212 | + return BadRequest(new ErrorResponse { Error = "Provider name is required" }); |
| 213 | + } |
| 214 | + |
| 215 | + var config = _configService.GetConfiguration(); |
| 216 | + |
| 217 | + // Check if provider has credentials configured |
| 218 | + var apiKey = ApiKeyResolver.GetApiKey(provider.ToLowerInvariant(), config); |
| 219 | + var providerInfo = TranslationProviderFactory.GetProviderInfos() |
| 220 | + .FirstOrDefault(p => p.Name.Equals(provider, StringComparison.OrdinalIgnoreCase)); |
| 221 | + |
| 222 | + if (providerInfo == null) |
| 223 | + { |
| 224 | + return BadRequest(new ErrorResponse { Error = $"Unknown provider: {provider}" }); |
| 225 | + } |
| 226 | + |
| 227 | + if (providerInfo.RequiresApiKey && string.IsNullOrWhiteSpace(apiKey)) |
| 228 | + { |
| 229 | + return Ok(new ProviderTestResponse |
| 230 | + { |
| 231 | + Success = false, |
| 232 | + Provider = provider, |
| 233 | + Message = "No API key configured for this provider" |
| 234 | + }); |
| 235 | + } |
| 236 | + |
| 237 | + // Try to create the provider and make a test translation |
| 238 | + try |
| 239 | + { |
| 240 | + var translationProvider = TranslationProviderFactory.Create(provider.ToLowerInvariant(), config); |
| 241 | + |
| 242 | + var testRequest = new TranslationRequest |
| 243 | + { |
| 244 | + SourceText = "Hello", |
| 245 | + SourceLanguage = "en", |
| 246 | + TargetLanguage = "es", |
| 247 | + Context = "test" |
| 248 | + }; |
| 249 | + |
| 250 | + var response = await translationProvider.TranslateAsync(testRequest); |
| 251 | + |
| 252 | + return Ok(new ProviderTestResponse |
| 253 | + { |
| 254 | + Success = true, |
| 255 | + Provider = provider, |
| 256 | + Message = $"Connection successful! Test translation: 'Hello' -> '{response.TranslatedText}'" |
| 257 | + }); |
| 258 | + } |
| 259 | + catch (Exception ex) |
| 260 | + { |
| 261 | + // Sanitize error message |
| 262 | + var message = ex.Message.Contains("API key") || ex.Message.Contains("authentication") |
| 263 | + ? "Authentication failed - check your API key" |
| 264 | + : ex.Message.Contains("rate limit") |
| 265 | + ? "Rate limit exceeded - try again later" |
| 266 | + : ex.Message.Contains("network") || ex is HttpRequestException |
| 267 | + ? "Network error - check your connection" |
| 268 | + : "Provider test failed"; |
| 269 | + |
| 270 | + return Ok(new ProviderTestResponse |
| 271 | + { |
| 272 | + Success = false, |
| 273 | + Provider = provider, |
| 274 | + Message = message |
| 275 | + }); |
| 276 | + } |
| 277 | + } |
| 278 | + catch (Exception) |
| 279 | + { |
| 280 | + return StatusCode(500, new ErrorResponse { Error = "An error occurred while processing your request" }); |
| 281 | + } |
| 282 | + } |
| 283 | + |
| 284 | + /// <summary> |
| 285 | + /// Enable or disable the secure credential store |
| 286 | + /// </summary> |
| 287 | + [HttpPut("secure-store")] |
| 288 | + public ActionResult<OperationResponse> SetSecureStoreEnabled([FromBody] SetSecureStoreRequest request) |
| 289 | + { |
| 290 | + try |
| 291 | + { |
| 292 | + var config = _configService.GetConfiguration() ?? new ConfigurationModel(); |
| 293 | + if (config.Translation == null) |
| 294 | + { |
| 295 | + config.Translation = new TranslationConfiguration(); |
| 296 | + } |
| 297 | + |
| 298 | + config.Translation.UseSecureCredentialStore = request.Enabled; |
| 299 | + _configService.SaveConfiguration(config); |
| 300 | + |
| 301 | + return Ok(new OperationResponse |
| 302 | + { |
| 303 | + Success = true, |
| 304 | + Message = request.Enabled |
| 305 | + ? "Secure credential store enabled" |
| 306 | + : "Secure credential store disabled" |
| 307 | + }); |
| 308 | + } |
| 309 | + catch (Exception) |
| 310 | + { |
| 311 | + return StatusCode(500, new ErrorResponse { Error = "An error occurred while processing your request" }); |
| 312 | + } |
| 313 | + } |
| 314 | +} |
| 315 | + |
| 316 | +// Request/Response models |
| 317 | +public class SetApiKeyRequest |
| 318 | +{ |
| 319 | + public string ApiKey { get; set; } = string.Empty; |
| 320 | +} |
| 321 | + |
| 322 | +public class SetSecureStoreRequest |
| 323 | +{ |
| 324 | + public bool Enabled { get; set; } |
| 325 | +} |
| 326 | + |
| 327 | +public class CredentialProviderInfo |
| 328 | +{ |
| 329 | + public string Provider { get; set; } = string.Empty; |
| 330 | + public string DisplayName { get; set; } = string.Empty; |
| 331 | + public bool RequiresApiKey { get; set; } |
| 332 | + public string? Source { get; set; } |
| 333 | + public bool IsConfigured { get; set; } |
| 334 | +} |
| 335 | + |
| 336 | +public class CredentialProvidersResponse |
| 337 | +{ |
| 338 | + public List<CredentialProviderInfo> Providers { get; set; } = new(); |
| 339 | + public bool UseSecureCredentialStore { get; set; } |
| 340 | +} |
| 341 | + |
| 342 | +public class CredentialSourceResponse |
| 343 | +{ |
| 344 | + public string Provider { get; set; } = string.Empty; |
| 345 | + public string? Source { get; set; } |
| 346 | + public bool IsConfigured { get; set; } |
| 347 | +} |
| 348 | + |
| 349 | +public class ProviderTestResponse |
| 350 | +{ |
| 351 | + public bool Success { get; set; } |
| 352 | + public string Provider { get; set; } = string.Empty; |
| 353 | + public string Message { get; set; } = string.Empty; |
| 354 | +} |
0 commit comments