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
46 changes: 46 additions & 0 deletions backend/src/CCE.Api.Common/Localization/Resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -476,3 +476,49 @@ EVALUATION_NOT_FOUND:
EVALUATION_SUBMITTED:
ar: "تم تقديم التقييم بنجاح"
en: "Evaluation submitted successfully"

# ─── Resource-specific messages (BRD Sprint 04) ───

RESOURCE_CREATED:
ar: "تم رفع المصدر بنجاح!"
en: "Resource uploaded successfully!"

RESOURCE_DELETED:
ar: "تم حذف المصدر بنجاح!"
en: "Resource deleted successfully!"

CONTENT_UPDATED:
ar: "تم التحديث بنجاح"
en: "Content updated successfully"

CONTENT_DELETED:
ar: "تم الحذف بنجاح"
en: "Content deleted successfully"

RESOURCE_DOWNLOAD_FAILED:
ar: "حدث خطأ أثناء محاولة تحميل المصدر. يرجى المحاولة مرة أخرى."
en: "An error occurred while downloading the resource. Please try again."

RESOURCE_UPLOAD_FAILED:
ar: "عذراً، حدثت مشكلة أثناء رفع المصدر."
en: "Sorry, a problem occurred while uploading the resource."

RESOURCE_DELETE_FAILED:
ar: "عذراً، حدثت مشكلة أثناء حذف المصدر."
en: "Sorry, a problem occurred while deleting the resource."

RESOURCE_NOT_FOUND_ALT:
ar: "عذراً، لا توجد مصادر حالياً."
en: "Sorry, there are no resources currently."

RESOURCE_DOWNLOAD_SUCCESS:
ar: "تم تحميل المصدر بنجاح! يمكنك الآن الوصول إلى المرفق من جهازك."
en: "Resource downloaded successfully! You can now access the attachment from your device."

RESOURCE_SHARE_SUCCESS:
ar: "تمت مشاركة المصدر بنجاح!"
en: "Resource shared successfully!"

RESOURCE_SHARE_FAILED:
ar: "حدث خطأ أثناء محاولة مشاركة المصدر. يرجى المحاولة مرة أخرى لاحقاً."
en: "An error occurred while trying to share the resource. Please try again later."
21 changes: 12 additions & 9 deletions backend/src/CCE.Api.External/Endpoints/EventsPublicEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using CCE.Api.Common.Extensions;
using CCE.Application.Content.Public;
using CCE.Application.Content.Public.Queries.GetPublicEventById;
using CCE.Application.Content.Public.Queries.ListPublicEvents;
Expand All @@ -17,15 +18,17 @@ public static IEndpointRouteBuilder MapEventsPublicEndpoints(this IEndpointRoute
events.MapGet("", async (
int? page, int? pageSize,
System.DateTimeOffset? from, System.DateTimeOffset? to,
string? topicSlug,
IMediator mediator, CancellationToken cancellationToken) =>
{
var query = new ListPublicEventsQuery(
Page: page ?? 1,
PageSize: pageSize ?? 20,
From: from,
To: to);
var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
To: to,
TopicSlug: topicSlug);
var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.AllowAnonymous()
.WithName("ListPublicEvents");
Expand All @@ -34,8 +37,8 @@ public static IEndpointRouteBuilder MapEventsPublicEndpoints(this IEndpointRoute
System.Guid id,
IMediator mediator, CancellationToken cancellationToken) =>
{
var dto = await mediator.Send(new GetPublicEventByIdQuery(id), cancellationToken).ConfigureAwait(false);
return dto is null ? Results.NotFound() : Results.Ok(dto);
var response = await mediator.Send(new GetPublicEventByIdQuery(id), cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.AllowAnonymous()
.WithName("GetPublicEventById");
Expand All @@ -44,10 +47,10 @@ public static IEndpointRouteBuilder MapEventsPublicEndpoints(this IEndpointRoute
System.Guid id,
IMediator mediator, CancellationToken cancellationToken) =>
{
var dto = await mediator.Send(new GetPublicEventByIdQuery(id), cancellationToken).ConfigureAwait(false);
if (dto is null)
return Results.NotFound();
var ics = IcsBuilder.ToIcs(dto);
var response = await mediator.Send(new GetPublicEventByIdQuery(id), cancellationToken).ConfigureAwait(false);
if (!response.Success)
return response.ToHttpResult();
var ics = IcsBuilder.ToIcs(response.Data!);
return Results.Text(ics, "text/calendar; charset=utf-8");
})
.AllowAnonymous()
Expand Down
15 changes: 8 additions & 7 deletions backend/src/CCE.Api.External/Endpoints/NewsPublicEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using CCE.Api.Common.Extensions;
using CCE.Application.Content.Public.Queries.GetPublicNewsBySlug;
using CCE.Application.Content.Public.Queries.ListPublicNews;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace CCE.Api.External.Endpoints;
Expand All @@ -14,15 +14,16 @@ public static IEndpointRouteBuilder MapNewsPublicEndpoints(this IEndpointRouteBu
var news = app.MapGroup("/api/news").WithTags("News");

news.MapGet("", async (
int? page, int? pageSize, bool? isFeatured,
int? page, int? pageSize, bool? isFeatured, string? topicSlug,
IMediator mediator, CancellationToken cancellationToken) =>
{
var query = new ListPublicNewsQuery(
Page: page ?? 1,
PageSize: pageSize ?? 20,
IsFeatured: isFeatured);
var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
IsFeatured: isFeatured,
TopicSlug: topicSlug);
var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.AllowAnonymous()
.WithName("ListPublicNews");
Expand All @@ -31,8 +32,8 @@ public static IEndpointRouteBuilder MapNewsPublicEndpoints(this IEndpointRouteBu
string slug,
IMediator mediator, CancellationToken cancellationToken) =>
{
var dto = await mediator.Send(new GetPublicNewsBySlugQuery(slug), cancellationToken).ConfigureAwait(false);
return dto is null ? Results.NotFound() : Results.Ok(dto);
var response = await mediator.Send(new GetPublicNewsBySlugQuery(slug), cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.AllowAnonymous()
.WithName("GetPublicNewsBySlug");
Expand Down
21 changes: 8 additions & 13 deletions backend/src/CCE.Api.External/Endpoints/ResourcesPublicEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using CCE.Api.Common.Extensions;
using CCE.Application.Common.Interfaces;
using CCE.Application.Content;
using CCE.Application.Content.Public;
Expand All @@ -19,15 +20,16 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo
var resources = app.MapGroup("/api/resources").WithTags("Resources");

resources.MapGet("", async (
int? page, int? pageSize,
int? page, int? pageSize, string? search,
System.Guid? categoryId, System.Guid? countryId, ResourceType? resourceType,
IMediator mediator, CancellationToken cancellationToken) =>
{
var query = new ListPublicResourcesQuery(
Page: page ?? 1, PageSize: pageSize ?? 20,
Search: search,
CategoryId: categoryId, CountryId: countryId, ResourceType: resourceType);
var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.AllowAnonymous()
.WithName("ListPublicResources");
Expand All @@ -36,8 +38,8 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo
System.Guid id,
IMediator mediator, CancellationToken cancellationToken) =>
{
var dto = await mediator.Send(new GetPublicResourceByIdQuery(id), cancellationToken).ConfigureAwait(false);
return dto is null ? Results.NotFound() : Results.Ok(dto);
var response = await mediator.Send(new GetPublicResourceByIdQuery(id), cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.AllowAnonymous()
.WithName("GetPublicResourceById");
Expand All @@ -50,21 +52,15 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo
IResourceViewCountRepository viewCounter,
CancellationToken cancellationToken) =>
{
// Load resource + asset metadata in a single round trip.
var resource = await db.Resources.FirstOrDefaultAsync(r => r.Id == id, cancellationToken).ConfigureAwait(false);
if (resource is null || resource.PublishedOn is null)
{
return Results.NotFound();
}

var asset = await db.AssetFiles.FirstOrDefaultAsync(a => a.Id == resource.AssetFileId, cancellationToken).ConfigureAwait(false);
if (asset is null)
{
return Results.NotFound();
}
if (asset.VirusScanStatus != VirusScanStatus.Clean)
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}

httpContext.Response.ContentType = asset.MimeType;
httpContext.Response.Headers.ContentDisposition =
Expand All @@ -73,7 +69,6 @@ public static IEndpointRouteBuilder MapResourcesPublicEndpoints(this IEndpointRo
await using var stream = await storage.OpenReadAsync(asset.Url, cancellationToken).ConfigureAwait(false);
await stream.CopyToAsync(httpContext.Response.Body, cancellationToken).ConfigureAwait(false);

// Fire-and-forget view-count bump (don't await; don't propagate exceptions).
_ = Task.Run(async () =>
{
try { await viewCounter.IncrementAsync(id, CancellationToken.None).ConfigureAwait(false); }
Expand Down
4 changes: 2 additions & 2 deletions backend/src/CCE.Api.External/appsettings.Production.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@
},
"ExternalApis": {
"CommunicationGateway": {
"BaseUrl": "https://cce-mocks.bonto.run",
"BaseUrl": "https://cce-mock.bonto.run",
"TimeoutSeconds": 30
},
"AdminAuthGateway": {
"BaseUrl": "https://cce-mocks.bonto.run",
"BaseUrl": "https://cce-mock.bonto.run",
"TimeoutSeconds": 30
}
},
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
12 changes: 12 additions & 0 deletions backend/src/CCE.Api.Internal/Endpoints/CreateEventRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace CCE.Api.Internal.Endpoints;

public sealed record CreateEventRequest(
string TitleAr, string TitleEn,
string DescriptionAr, string DescriptionEn,
System.DateTimeOffset StartsOn,
System.DateTimeOffset EndsOn,
string? LocationAr,
string? LocationEn,
string? OnlineMeetingUrl,
string? FeaturedImageUrl,
System.Guid TopicId);
5 changes: 5 additions & 0 deletions backend/src/CCE.Api.Internal/Endpoints/CreateNewsRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace CCE.Api.Internal.Endpoints;

public sealed record CreateNewsRequest(
string TitleAr, string TitleEn, string ContentAr, string ContentEn,
System.Guid TopicId, string? FeaturedImageUrl);
14 changes: 14 additions & 0 deletions backend/src/CCE.Api.Internal/Endpoints/CreateResourceRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using CCE.Domain.Content;

namespace CCE.Api.Internal.Endpoints;

public sealed record CreateResourceRequest(
string TitleAr,
string TitleEn,
string DescriptionAr,
string DescriptionEn,
ResourceType ResourceType,
System.Guid CategoryId,
System.Guid? CountryId,
System.Guid AssetFileId,
List<System.Guid> CountryIds);
69 changes: 25 additions & 44 deletions backend/src/CCE.Api.Internal/Endpoints/EventEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using CCE.Api.Common.Extensions;
using CCE.Application.Content.Commands.CreateEvent;
using CCE.Application.Content.Commands.DeleteEvent;
using CCE.Application.Content.Commands.RescheduleEvent;
Expand All @@ -7,7 +8,6 @@
using CCE.Domain;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace CCE.Api.Internal.Endpoints;
Expand All @@ -20,33 +20,38 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder

events.MapGet("", async (
int? page, int? pageSize, string? search,
System.DateTimeOffset? fromDate, System.DateTimeOffset? toDate,
System.DateTimeOffset? fromDate, System.DateTimeOffset? toDate, System.Guid? topicId,
IMediator mediator, CancellationToken cancellationToken) =>
{
var query = new ListEventsQuery(page ?? 1, pageSize ?? 20, search, fromDate, toDate);
var result = await mediator.Send(query, cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
var query = new ListEventsQuery(page ?? 1, pageSize ?? 20, search, fromDate, toDate, topicId);
var response = await mediator.Send(query, cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.RequireAuthorization(Permissions.Event_Manage)
.WithName("ListEvents");

events.MapGet("/{id:guid}", async (System.Guid id, IMediator mediator, CancellationToken cancellationToken) =>
events.MapGet("/{id:guid}", async (
System.Guid id,
IMediator mediator, CancellationToken cancellationToken) =>
{
var dto = await mediator.Send(new GetEventByIdQuery(id), cancellationToken).ConfigureAwait(false);
return dto is null ? Results.NotFound() : Results.Ok(dto);
var response = await mediator.Send(new GetEventByIdQuery(id), cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.RequireAuthorization(Permissions.Event_Manage)
.WithName("GetEventById");

events.MapPost("", async (CreateEventRequest body, IMediator mediator, CancellationToken cancellationToken) =>
events.MapPost("", async (
CreateEventRequest body,
IMediator mediator, CancellationToken cancellationToken) =>
{
var cmd = new CreateEventCommand(
body.TitleAr, body.TitleEn, body.DescriptionAr, body.DescriptionEn,
body.StartsOn, body.EndsOn,
body.LocationAr, body.LocationEn,
body.OnlineMeetingUrl, body.FeaturedImageUrl);
var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false);
return Results.Created($"/api/admin/events/{dto.Id}", dto);
body.OnlineMeetingUrl, body.FeaturedImageUrl,
body.TopicId);
var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.RequireAuthorization(Permissions.Event_Manage)
.WithName("CreateEvent");
Expand All @@ -56,16 +61,15 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder
UpdateEventRequest body,
IMediator mediator, CancellationToken cancellationToken) =>
{
var rowVersion = string.IsNullOrEmpty(body.RowVersion) ? System.Array.Empty<byte>() : System.Convert.FromBase64String(body.RowVersion);
var cmd = new UpdateEventCommand(
id,
body.TitleAr, body.TitleEn,
body.DescriptionAr, body.DescriptionEn,
body.LocationAr, body.LocationEn,
body.OnlineMeetingUrl, body.FeaturedImageUrl,
rowVersion);
var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false);
return dto is null ? Results.NotFound() : Results.Ok(dto);
body.TopicId);
var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.RequireAuthorization(Permissions.Event_Manage)
.WithName("UpdateEvent");
Expand All @@ -75,10 +79,9 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder
RescheduleEventRequest body,
IMediator mediator, CancellationToken cancellationToken) =>
{
var rowVersion = string.IsNullOrEmpty(body.RowVersion) ? System.Array.Empty<byte>() : System.Convert.FromBase64String(body.RowVersion);
var cmd = new RescheduleEventCommand(id, body.StartsOn, body.EndsOn, rowVersion);
var dto = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false);
return dto is null ? Results.NotFound() : Results.Ok(dto);
var cmd = new RescheduleEventCommand(id, body.StartsOn, body.EndsOn);
var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.RequireAuthorization(Permissions.Event_Manage)
.WithName("RescheduleEvent");
Expand All @@ -87,34 +90,12 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder
System.Guid id,
IMediator mediator, CancellationToken cancellationToken) =>
{
await mediator.Send(new DeleteEventCommand(id), cancellationToken).ConfigureAwait(false);
return Results.NoContent();
var response = await mediator.Send(new DeleteEventCommand(id), cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
})
.RequireAuthorization(Permissions.Event_Manage)
.WithName("DeleteEvent");

return app;
}
}

public sealed record CreateEventRequest(
string TitleAr, string TitleEn,
string DescriptionAr, string DescriptionEn,
System.DateTimeOffset StartsOn,
System.DateTimeOffset EndsOn,
string? LocationAr,
string? LocationEn,
string? OnlineMeetingUrl,
string? FeaturedImageUrl);

public sealed record UpdateEventRequest(
string TitleAr, string TitleEn,
string DescriptionAr, string DescriptionEn,
string? LocationAr, string? LocationEn,
string? OnlineMeetingUrl, string? FeaturedImageUrl,
string RowVersion);

public sealed record RescheduleEventRequest(
System.DateTimeOffset StartsOn,
System.DateTimeOffset EndsOn,
string RowVersion);
Loading