Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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: {}. Content moderation skipped.", ex.Message);
Copy link

Copilot AI Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message uses a placeholder '{}' which is not standard for .NET logging. Use '{Message}' or positional parameters like '{0}' for proper structured logging.

Copilot uses AI. Check for mistakes.
}
}

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(statusCode: ex.Status.ToString(), errorCode: ex.ErrorCode ?? "", ex.Message);
Copy link

Copilot AI Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The named parameter 'statusCode:' is inconsistent with the other unnamed parameters. Either use named parameters for all arguments or remove the named parameter for consistency.

Suggested change
_logger.LogContentModerationError(statusCode: ex.Status.ToString(), errorCode: ex.ErrorCode ?? "", ex.Message);
_logger.LogContentModerationError(ex.Status.ToString(), ex.ErrorCode ?? "", ex.Message);

Copilot uses AI. Check for mistakes.
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;
}
}