Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -459,4 +459,6 @@ src/emailpaywall.client/.svelte-kit

# Include example tfvars files
!**/*.tfvars.example
!**/terraform.tfvars.example
!**/terraform.tfvars.example

**/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public interface IObjectStorageService {
Task<Uri> GetFileUri(string containerName, string fileName);
Task<BinaryData> GetFile(string containerName, string fileName);
Task<bool> IsFileExists(string containerName, string fileName);
Task<bool> PassesContentModeration(BinaryData binaryData);
}
6 changes: 6 additions & 0 deletions src/Evently.Server/Common/Domains/Models/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public sealed class Settings {
[NotMapped] public AuthSetting Authentication { get; init; } = new();

[NotMapped] public EmailSettings EmailSettings { get; init; } = new();
[NotMapped] public AzureAIFoundry AzureAiFoundry { get; init; } = new();
}

[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
Expand All @@ -32,4 +33,9 @@ public sealed class OAuthSetting {
public sealed class EmailSettings {
public string ActualFrom { get; init; } = string.Empty;
public string SmtpPassword { get; init; } = string.Empty;
}

public sealed class AzureAIFoundry {
public string ContentSafetyKey { get; init; } = string.Empty;
public string ContentSafetyEndpoint { get; init; } = string.Empty;
}
1 change: 1 addition & 0 deletions src/Evently.Server/Evently.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.ContentSafety" Version="1.0.0"/>
<PackageReference Include="Azure.Storage.Blobs" Version="12.25.0"/>
<PackageReference Include="FluentValidation" Version="12.0.0"/>
<PackageReference Include="HtmlRenderer.PdfSharp" Version="1.5.0.6"/>
Expand Down
23 changes: 23 additions & 0 deletions src/Evently.Server/Features/Files/Services/ObjectStorageService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Azure;
using Azure.AI.ContentSafety;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Evently.Server.Common.Domains.Interfaces;
Expand All @@ -11,6 +12,9 @@ namespace Evently.Server.Features.Files.Services;
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 async Task<Uri> UploadFile(string containerName, string fileName, BinaryData binaryData,
string mimeType = "application/octet-stream") {
Expand Down Expand Up @@ -65,4 +69,23 @@ public async Task<BinaryData> GetFile(string containerName, string fileName) {
BinaryData data = BinaryData.FromBytes(bytes);
return data;
}

public async Task<bool> PassesContentModeration(BinaryData binaryData) {
ContentSafetyImageData image = new(binaryData);
AnalyzeImageOptions request = new(image);
Response<AnalyzeImageResult> response;
try {
response = await _contentSafetyClient.AnalyzeImageAsync(request);
} catch (RequestFailedException ex) {
Console.WriteLine("Analyze image failed.\nStatus code: {0}, Error code: {1}, Error message: {2}", ex.Status, ex.ErrorCode, ex.Message);
Comment thread
eugbyte marked this conversation as resolved.
Outdated
throw;
}

AnalyzeImageResult result = response.Value;
int? score = result.CategoriesAnalysis
.Select(v => v.Severity)
.Aggregate((a, b) => a + b)
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

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

The aggregation logic assumes all severity values are integers that can be summed. Consider using Sum() instead of Aggregate((a, b) => a + b) for clarity and better null handling.

Suggested change
.Aggregate((a, b) => a + b)
.Sum()

Copilot uses AI. Check for mistakes.
?? 0;
Comment on lines +88 to +89
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

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

The null-conditional operator ?? is applied to the wrong expression. If result.CategoriesAnalysis is empty, Aggregate will throw an exception before the null check. Consider using DefaultIfEmpty() or checking if the collection is empty first.

Suggested change
.Aggregate((a, b) => a + b)
?? 0;
.DefaultIfEmpty(0)
.Aggregate((a, b) => a + b);

Copilot uses AI. Check for mistakes.
return result.CategoriesAnalysis.Count != 0 && score == 0;
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

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

The logic for determining content safety is incorrect. When CategoriesAnalysis.Count is 0, the method returns false (content is unsafe), but when there are no categories to analyze, the content should be considered safe. Also, the aggregate will throw an exception if the collection is empty.

Suggested change
int? score = result.CategoriesAnalysis
.Select(v => v.Severity)
.Aggregate((a, b) => a + b)
?? 0;
return result.CategoriesAnalysis.Count != 0 && score == 0;
if (result.CategoriesAnalysis.Count == 0)
{
// No categories to analyze, consider content safe
return true;
}
int score = result.CategoriesAnalysis.Sum(v => v.Severity);
return score == 0;

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ public async Task<ActionResult<Gathering>> DeleteGathering(long gatheringId) {
private async Task<Uri> UploadCoverImage(long gatheringId, IFormFile coverImg) {
string fileName = $"gatherings/{gatheringId}/cover-image{Path.GetExtension(coverImg.FileName)}";
BinaryData binaryData = await coverImg.ToBinaryData();
bool isContentSafe = await objectStorageService.PassesContentModeration(binaryData);
if (!isContentSafe) {
return new Uri(string.Empty, UriKind.RelativeOrAbsolute);
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

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

Returning an empty URI when content moderation fails is misleading. Consider throwing an exception or returning a specific error response to clearly indicate the image was rejected due to content policy violations.

Copilot uses AI. Check for mistakes.
}
return await objectStorageService.UploadFile(_containerName,
fileName,
binaryData,
Expand Down
26 changes: 0 additions & 26 deletions src/Evently.Server/appsettings.Development.json

This file was deleted.

23 changes: 22 additions & 1 deletion src/Evently.Server/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,26 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"ConnectionStrings": {
"WebApiDatabase": "Data Source=(localdb)\\MSSQLLocalDB;Database=evently;Integrated Security=True;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Application Name=\"SQL Server Management Studio\";Command Timeout=0;"
},
"StorageAccount": {
"AccountName": "evently-dev-images",
"AzureStorageConnectionString": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
},
"AzureAIFoundry": {
"ContentSafetyKey": "<ai-foundary-content-safety-key>",
"ContentSafetyEndpoint": "<ai-foundary-content-safety-api>"
Comment thread
eugbyte marked this conversation as resolved.
Outdated
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

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

Corrected spelling of 'foundary' to 'foundry'.

Suggested change
"ContentSafetyKey": "<ai-foundary-content-safety-key>",
"ContentSafetyEndpoint": "<ai-foundary-content-safety-api>"
"ContentSafetyKey": "<ai-foundry-content-safety-key>",
"ContentSafetyEndpoint": "<ai-foundry-content-safety-api>"

Copilot uses AI. Check for mistakes.
},
"Authentication": {
"Google": {
"ClientId": "<your-google-client-id>",
"ClientSecret": "<your-google-client-secret>"
}
},
"EmailSettings": {
"ActualFrom": "<your-email@example.com>",
"SmtpPassword": "<your-app-password>"
}
}
6 changes: 4 additions & 2 deletions src/evently.client/src/lib/components/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ export function Card({ gathering, accountId }: CardProps): JSX.Element {
);

// ignore img-src for now
const hash: number = hashString(gathering.name);
imgSrc = hash % 2 === 0 ? Placeholder1 : Placeholder2;
if (imgSrc == null || imgSrc.length === 0) {
const hash: number = hashString(gathering.name);
imgSrc = hash % 2 === 0 ? Placeholder1 : Placeholder2;
}

if (title.length > 30) {
title = title.substring(0, 30) + "...";
Expand Down
4 changes: 4 additions & 0 deletions src/evently.client/src/lib/services/gathering-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ function toFormData(gatheringDto: GatheringReqDto, coverImg?: File | null): Form
formData.set(key, value);
}
}

formData.delete("gatheringCategoryDetails");
if (gatheringDto.gatheringCategoryDetails.length === 0) {
formData.set("gatheringCategoryDetails", "[]");
}

for (let i = 0; i < gatheringDto.gatheringCategoryDetails.length; i++) {
const detail: GatheringCategoryDetailReqDto = gatheringDto.gatheringCategoryDetails[i];
Expand Down