diff --git a/deploy/Terraform/ai-content-moderation.tf b/deploy/Terraform/ai-content-moderation.tf new file mode 100644 index 0000000..65b99f8 --- /dev/null +++ b/deploy/Terraform/ai-content-moderation.tf @@ -0,0 +1,8 @@ +# Azure AI Foundry Content Safety resource +resource "azurerm_cognitive_account" "content_safety" { + name = "cs-evently-dev-sea" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + kind = "ContentSafety" + sku_name = "F0" +} \ No newline at end of file diff --git a/deploy/Terraform/container-app.tf b/deploy/Terraform/container-app.tf index ceba8de..e0cb185 100644 --- a/deploy/Terraform/container-app.tf +++ b/deploy/Terraform/container-app.tf @@ -168,26 +168,26 @@ resource "azurerm_container_app" "app" { value = "Warning" } - # General Settings env { - name = "AllowedHosts" - value = "*" + name = "AzureAIFoundry__ContentSafetyKey" + value = azurerm_cognitive_account.content_safety.primary_access_key } - # Environment indicator env { - name = "ASPNETCORE_ENVIRONMENT" - value = "Production" + name = "AzureAIFoundry__ContentSafetyEndpoint" + value = azurerm_cognitive_account.content_safety.endpoint } + # General Settings env { - name = "AzureAIFoundry__ContentSafetyKey" - secret_name = "content-safety-key" + name = "AllowedHosts" + value = "*" } + # Environment indicator env { - name = "AzureAIFoundry__ContentSafetyEndpoint" - value = var.content_safety_api + name = "ASPNETCORE_ENVIRONMENT" + value = "Production" } } } @@ -226,9 +226,4 @@ resource "azurerm_container_app" "app" { value = var.smtp_password } - secret { - name = "content-safety-key" - value = var.content_safety_key - } - } \ No newline at end of file diff --git a/deploy/Terraform/variables.tf b/deploy/Terraform/variables.tf index b53efd6..8c4c7c2 100644 --- a/deploy/Terraform/variables.tf +++ b/deploy/Terraform/variables.tf @@ -34,15 +34,4 @@ variable "smtp_password" { description = "SMTP password for email sending" type = string sensitive = true -} - -variable "content_safety_key" { - description = "Azure AI foundry content safety key" - type = string - sensitive = true -} - -variable "content_safety_api" { - description = "Azure AI foundry content safety api endpoint" - type = string } \ No newline at end of file diff --git a/src/Evently.Server/Common/Domains/Models/Settings.cs b/src/Evently.Server/Common/Domains/Models/Settings.cs index 75348cf..073e9e7 100644 --- a/src/Evently.Server/Common/Domains/Models/Settings.cs +++ b/src/Evently.Server/Common/Domains/Models/Settings.cs @@ -35,6 +35,8 @@ public sealed class EmailSettings { public string SmtpPassword { get; init; } = string.Empty; } +[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] +// ReSharper disable once InconsistentNaming public sealed class AzureAIFoundry { public string ContentSafetyKey { get; init; } = string.Empty; public string ContentSafetyEndpoint { get; init; } = string.Empty; diff --git a/src/Evently.Server/Common/Extensions/LoggerExtension.cs b/src/Evently.Server/Common/Extensions/LoggerExtension.cs index 56f6a79..eb99e5f 100644 --- a/src/Evently.Server/Common/Extensions/LoggerExtension.cs +++ b/src/Evently.Server/Common/Extensions/LoggerExtension.cs @@ -28,7 +28,7 @@ public static partial void LogSuccessEmail( Message = "Error occurred at {context}: {errorMsg}")] public static partial void LogErrorContext( this ILogger logger, string context, string errorMsg); - + [LoggerMessage( EventId = 5, Level = LogLevel.Error, diff --git a/src/Evently.Server/Features/Bookings/Services/BookingService.cs b/src/Evently.Server/Features/Bookings/Services/BookingService.cs index 1ee3973..5d2e709 100644 --- a/src/Evently.Server/Features/Bookings/Services/BookingService.cs +++ b/src/Evently.Server/Features/Bookings/Services/BookingService.cs @@ -126,8 +126,4 @@ public async Task RenderTicket(string bookingId) { return await mediaRenderer.RenderComponentHtml(props); } - - public async Task Exists(string bookingId) { - return await db.Bookings.AnyAsync(b => b.BookingId == bookingId); - } } \ No newline at end of file diff --git a/src/Evently.Server/Features/Emails/Services/EmailBackgroundService.cs b/src/Evently.Server/Features/Emails/Services/EmailBackgroundService.cs index 237a1bb..d6e9a7a 100644 --- a/src/Evently.Server/Features/Emails/Services/EmailBackgroundService.cs +++ b/src/Evently.Server/Features/Emails/Services/EmailBackgroundService.cs @@ -1,7 +1,7 @@ using Evently.Server.Common.Domains.Entities; using Evently.Server.Common.Domains.Interfaces; +using Evently.Server.Common.Extensions; using System.Threading.Channels; -using LoggerExtension=Evently.Server.Common.Extensions.LoggerExtension; namespace Evently.Server.Features.Emails.Services; @@ -26,7 +26,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { string html = await bookingService.RenderTicket(bookingId); await emailerAdapter.SendEmailAsync("noreply@evently", account.Email, "Test QR ticket", html); - LoggerExtension.LogSuccessEmail(logger, account.Email); + logger.LogSuccessEmail(account.Email); } catch (Exception ex) { logger.LogError("email error: {}", ex.Message); } diff --git a/src/Evently.Server/Features/Files/Services/ObjectStorageService.cs b/src/Evently.Server/Features/Files/Services/ObjectStorageService.cs index 285c02b..add97e3 100644 --- a/src/Evently.Server/Features/Files/Services/ObjectStorageService.cs +++ b/src/Evently.Server/Features/Files/Services/ObjectStorageService.cs @@ -10,12 +10,25 @@ namespace Evently.Server.Features.Files.Services; // Based on https://tinyurl.com/5pam66xn -public sealed class ObjectStorageService(IOptions settings, ILogger logger) : IObjectStorageService { - private readonly BlobServiceClient _blobServiceClient = - new(settings.Value.StorageAccount.AzureStorageConnectionString); - private readonly ContentSafetyClient _contentSafetyClient = new( - endpoint: new Uri(settings.Value.AzureAiFoundry.ContentSafetyEndpoint), - credential: new AzureKeyCredential(settings.Value.AzureAiFoundry.ContentSafetyKey)); +public sealed class ObjectStorageService : IObjectStorageService { + private readonly BlobServiceClient _blobServiceClient; + private readonly ContentSafetyClient? _contentSafetyClient; + private readonly ILogger _logger; + + public ObjectStorageService(IOptions settings, ILogger logger) { + _logger = logger; + _blobServiceClient = + new BlobServiceClient(settings.Value.StorageAccount.AzureStorageConnectionString); + + try { + _contentSafetyClient = new ContentSafetyClient( + endpoint: new Uri(settings.Value.AzureAiFoundry.ContentSafetyEndpoint), + credential: new AzureKeyCredential(settings.Value.AzureAiFoundry.ContentSafetyKey)); + } catch (Exception ex) { + // silence the error + _logger.LogError("error creating content safety client: {message}. Content moderation skipped.", ex.Message); + } + } public async Task UploadFile(string containerName, string fileName, BinaryData binaryData, string mimeType = "application/octet-stream") { @@ -63,7 +76,7 @@ public async Task GetFile(string containerName, string fileName) { try { await blobClient.DownloadToAsync(ms); } catch (Exception ex) { - logger.LogError("error getting file: {}", ex.Message); + _logger.LogError("error getting file: {}", ex.Message); } byte[] bytes = ms.ToArray(); @@ -72,21 +85,25 @@ public async Task GetFile(string containerName, string fileName) { } public async Task PassesContentModeration(BinaryData binaryData) { + if (_contentSafetyClient is null) { + return true; + } + ContentSafetyImageData image = new(binaryData); AnalyzeImageOptions request = new(image); Response response; try { response = await _contentSafetyClient.AnalyzeImageAsync(request); } catch (RequestFailedException ex) { - logger.LogContentModerationError(ex.Status.ToString(), ex.ErrorCode ?? "", ex.Message); + _logger.LogContentModerationError(ex.Status.ToString(), ex.ErrorCode ?? "", ex.Message); throw; } AnalyzeImageResult result = response.Value; - int? score = result.CategoriesAnalysis - .Select(v => v.Severity) - .Aggregate((a, b) => a + b) - ?? 0; - return score == 0; + int dangerScore = result.CategoriesAnalysis + .Select(v => v.Severity ?? 0) + .DefaultIfEmpty(0) + .Aggregate((a, b) => a + b); + return dangerScore == 0; } } \ No newline at end of file