Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions deploy/Terraform/ai-content-moderation.tf
Original file line number Diff line number Diff line change
@@ -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"
}
25 changes: 10 additions & 15 deletions deploy/Terraform/container-app.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand Down Expand Up @@ -226,9 +226,4 @@ resource "azurerm_container_app" "app" {
value = var.smtp_password
}

secret {
name = "content-safety-key"
value = var.content_safety_key
}

}
11 changes: 0 additions & 11 deletions deploy/Terraform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions src/Evently.Server/Common/Domains/Models/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Evently.Server/Common/Extensions/LoggerExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,4 @@ public async Task<string> RenderTicket(string bookingId) {

return await mediaRenderer.RenderComponentHtml<Ticket>(props);
}

public async Task<bool> Exists(string bookingId) {
return await db.Bookings.AnyAsync(b => b.BookingId == bookingId);
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
}
Expand Down
43 changes: 30 additions & 13 deletions src/Evently.Server/Features/Files/Services/ObjectStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,25 @@
namespace Evently.Server.Features.Files.Services;

// Based on https://tinyurl.com/5pam66xn
public sealed class ObjectStorageService(IOptions<Settings> settings, ILogger<ObjectStorageService> 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<ObjectStorageService> _logger;

public ObjectStorageService(IOptions<Settings> settings, ILogger<ObjectStorageService> 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<Uri> UploadFile(string containerName, string fileName, BinaryData binaryData,
string mimeType = "application/octet-stream") {
Expand Down Expand Up @@ -63,7 +76,7 @@ public async Task<BinaryData> 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();
Expand All @@ -72,21 +85,25 @@ public async Task<BinaryData> GetFile(string containerName, string fileName) {
}

public async Task<bool> PassesContentModeration(BinaryData binaryData) {
if (_contentSafetyClient is null) {
return true;
}

ContentSafetyImageData image = new(binaryData);
AnalyzeImageOptions request = new(image);
Response<AnalyzeImageResult> 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;
}
}