Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
888cfb3
Add SignalR Package
Mahmoud-Ayman-Saleh Aug 17, 2025
ad90d92
Add Notification Hub
Mahmoud-Ayman-Saleh Aug 17, 2025
47f9e2b
Implement NotificationDto
Mahmoud-Ayman-Saleh Aug 18, 2025
fe6259a
Implement Notification Repository
Mahmoud-Ayman-Saleh Aug 18, 2025
6c8330e
fix: update notification model and repository for API compliance
Mahmoud-Ayman-Saleh Aug 18, 2025
0f1a29c
Implement Notification Services
Mahmoud-Ayman-Saleh Aug 19, 2025
e65c410
fix: NotificationDto by adding PaginationDto and implement manual map…
Mahmoud-Ayman-Saleh Aug 19, 2025
7432403
Implement Notification Controller
Mahmoud-Ayman-Saleh Aug 20, 2025
9125016
Implement notification hub
Mahmoud-Ayman-Saleh Aug 20, 2025
065fdac
Add Dependency injection
Mahmoud-Ayman-Saleh Aug 20, 2025
b2dca6d
refactor: Remove redundant actor fields and resolve actors via Resour…
Mahmoud-Ayman-Saleh Aug 24, 2025
fda6755
Remove NotificationResponseType that's because the frontend already s…
Mahmoud-Ayman-Saleh Aug 24, 2025
bf3cefd
Refactor notifications to use UnitOfWork and Repository pattern
Mahmoud-Ayman-Saleh Aug 24, 2025
d7bd9cf
Update Naming in Notification model and apply migrations for it
Mahmoud-Ayman-Saleh Aug 24, 2025
3e53d29
Enable Swagger
Mahmoud-Ayman-Saleh Aug 24, 2025
75057f0
Add Default Values to CreatedAt, UpdatedAt and DeletedAt
Mahmoud-Ayman-Saleh Aug 25, 2025
cb39a85
Merge branch 'Develop' into mahmoud-notif-patch
Mahmoud-Ayman-Saleh Aug 25, 2025
dcc35e8
Finish notification unit test and update notification hub
Mahmoud-Ayman-Saleh Aug 28, 2025
73b3fba
Apply Auth and update all files that depend on it
Mahmoud-Ayman-Saleh Aug 28, 2025
7e45bdb
Merge branch 'Develop' into mahmoud-notif-patch
Mahmoud-Ayman-Saleh Sep 20, 2025
ef94e30
Apply ServiceResult in Notifications Service
Mahmoud-Ayman-Saleh Sep 20, 2025
97620e4
fix(auth): Use standard JWT NameIdentifier claim for user ID extraction
Mahmoud-Ayman-Saleh Sep 20, 2025
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
29 changes: 28 additions & 1 deletion AskFm/AskFm.API/AskFm.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,39 @@
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.9.11" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />

</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AskFm.BLL\AskFm.BLL.csproj" />
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AskFm.BLL\AskFm.BLL.csproj" />
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
</Project>
120 changes: 120 additions & 0 deletions AskFm/AskFm.API/Controllers/NotificationController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System.Security.Claims;
using AskFm.BLL.DTO;
using AskFm.BLL.Services;
using AskFm.DAL.Enums;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace AskFm.API.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class NotificationController : ControllerBase
{
private readonly INotificationService _notificationService;

public NotificationController(INotificationService notificationService)
{
_notificationService = notificationService;
}

[HttpGet]
public async Task<IActionResult> GetUserNotifications([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
{
try
{
var userId = GetCurrentUserId();
var notifications = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize);
return Ok(notifications);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}

[HttpGet("type/{category}")]
public async Task<IActionResult> GetNotificationsByType(string category, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
{
try
{
var userId = GetCurrentUserId();
var response = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize);
return Ok(response);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}

[HttpPut("{notificationId}/read")]
public async Task<IActionResult> MarkNotificationAsRead(int notificationId)
{
try
{
var userId = GetCurrentUserId();
var result = await _notificationService.MarkNotificationAsRead(notificationId, userId);
return Ok(new { message = result });
}
catch (InvalidOperationException ex)
{
return NotFound(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
return Forbid(ex.Message);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}

[HttpPut("read-all")]
public async Task<IActionResult> MarkAllNotificationsAsRead()
{
try
{
var userId = GetCurrentUserId();
var result = await _notificationService.MarkAllNotificationsAsRead(userId);
return Ok(new { message = result });
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}

[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> CreateNotification([FromBody] CreateNotificationRequest request)
{
try
{
await _notificationService.CreateNotification(request.UserId, request.Type, request.ResourceId, request.Message);
return Ok(new { message = "Notification created successfully" });
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}

private int GetCurrentUserId()
{
// Use the standard NameIdentifier claim
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out int userId))
{
throw new UnauthorizedAccessException("Invalid user token");
}
return userId;
}
}
}
68 changes: 65 additions & 3 deletions AskFm/AskFm.API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text;
using AskFm.BLL.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using AskFm.DAL;
Expand All @@ -9,10 +8,13 @@
using DotNetEnv;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore.Proxies;
using AskFm.BLL.Hub;
using AskFm.BLL.Services;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using AskFm.BLL.Services.UserIdentityService;
using Swashbuckle.AspNetCore.SwaggerGen;
using Castle.Components.DictionaryAdapter.Xml;

namespace AskFm.API;
Expand Down Expand Up @@ -43,6 +45,9 @@ public static void Main(string[] args)
.UseSqlServer(ConnectionString));

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<INotificationRepository, NotificationRepository>();
builder.Services.AddScoped<INotificationService, NotificationService>();

builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ICommentLikeService, CommentLikeService>();
Expand Down Expand Up @@ -91,6 +96,39 @@ public static void Main(string[] args)
throw new Exception("jwtOptions is null");
}

// Enhanced SignalR Configuration
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
options.HandshakeTimeout = TimeSpan.FromSeconds(15);
});

// CORS Configuration for SignalR
builder.Services.AddCors(options =>
{
options.AddPolicy("SignalRPolicy", policy =>
{
// Option 1: Allow any origin (for development only)
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();

// Option 2: Specific origins (uncomment and modify when you know frontend URLs)
// policy.WithOrigins(
// "http://localhost:3000", // React default
// "http://localhost:4200", // Angular default
// "http://localhost:8080", // Vue default
// "http://localhost:5173", // Vite default
// "https://yourdomain.com" // Production domain
// )
// .AllowAnyMethod()
// .AllowAnyHeader()
// .AllowCredentials();
});
});
Comment on lines +108 to +130
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t ship AllowAnyOrigin; gate CORS by environment and allow credentials for known origins.

As-is, any site can open a WebSocket to your hub in production. Tighten CORS.

-        builder.Services.AddCors(options =>
-        {
-            options.AddPolicy("SignalRPolicy", policy =>
-            {
-                // Option 1: Allow any origin (for development only)
-                policy.AllowAnyOrigin()
-                      .AllowAnyMethod()
-                      .AllowAnyHeader();
+        builder.Services.AddCors(options =>
+        {
+            options.AddPolicy("SignalRPolicy", policy =>
+            {
+                if (builder.Environment.IsDevelopment())
+                {
+                    policy.AllowAnyOrigin()
+                          .AllowAnyMethod()
+                          .AllowAnyHeader();
+                }
+                else
+                {
+                    var origins = (Environment.GetEnvironmentVariable("FRONTEND_ORIGINS") ?? string.Empty)
+                        .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+                    policy.WithOrigins(origins)
+                          .AllowAnyMethod()
+                          .AllowAnyHeader()
+                          .AllowCredentials();
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// CORS Configuration for SignalR
builder.Services.AddCors(options =>
{
options.AddPolicy("SignalRPolicy", policy =>
{
// Option 1: Allow any origin (for development only)
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
// Option 2: Specific origins (uncomment and modify when you know frontend URLs)
// policy.WithOrigins(
// "http://localhost:3000", // React default
// "http://localhost:4200", // Angular default
// "http://localhost:8080", // Vue default
// "http://localhost:5173", // Vite default
// "https://yourdomain.com" // Production domain
// )
// .AllowAnyMethod()
// .AllowAnyHeader()
// .AllowCredentials();
});
});
// CORS Configuration for SignalR
builder.Services.AddCors(options =>
{
options.AddPolicy("SignalRPolicy", policy =>
{
if (builder.Environment.IsDevelopment())
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}
else
{
var origins = (Environment.GetEnvironmentVariable("FRONTEND_ORIGINS") ?? string.Empty)
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
policy.WithOrigins(origins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}
});
});
🤖 Prompt for AI Agents
In AskFm/AskFm.API/Program.cs around lines 75 to 97, the CORS policy currently
uses AllowAnyOrigin which is unsafe for production; update the policy to gate
behavior by environment: in non-production (Development) allow permissive CORS
for local dev, but in Production read a configured list of allowed origins
(e.g., from appsettings or an environment variable) and call
policy.WithOrigins(...).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
ensure you remove AllowAnyOrigin in production, apply the "SignalRPolicy" to the
SignalR hub endpoints, and validate the origins list is parsed and empty-check
guarded to avoid accidentally falling back to AllowAnyOrigin.


builder.Services.Configure<JwtOptions>(Options =>
{
Options.Issuer = Environment.GetEnvironmentVariable("ISSUER");
Expand Down Expand Up @@ -120,6 +158,21 @@ public static void Main(string[] args)
ClockSkew = TimeSpan.FromMinutes(0)
};

// Enable JWT authentication for SignalR
Options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;

if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/notificationHub"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
builder.Services.AddIdentity<ApplicationUser, IdentityRole<int>>(options =>
Expand Down Expand Up @@ -167,17 +220,26 @@ public static void Main(string[] args)
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.MapOpenApi();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "api");
});
}

app.UseHttpsRedirection();

// Apply CORS before authentication
app.UseCors("SignalRPolicy");

app.UseAuthentication();
app.UseAuthorization();


app.MapControllers();

// Map SignalR Hub
app.MapHub<NotificationHub>("/notificationHub");

app.Run();
}
}
2 changes: 1 addition & 1 deletion AskFm/AskFm.API/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5180",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Expand Down
30 changes: 15 additions & 15 deletions AskFm/AskFm.BLL/AskFm.BLL.csproj
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\AskFm.DAL\AskFm.DAL.csproj"/>
<ProjectReference Include="..\Shared\Shared.csproj"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="AutoMapper" Version="15.0.1" />
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AskFm.DAL\AskFm.DAL.csproj"/>
<ProjectReference Include="..\Shared\Shared.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>
<PackageReference Include="AutoMapper" Version="15.0.1"/>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
</ItemGroup>

</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions AskFm/AskFm.BLL/DTO/ActorDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace AskFm.BLL.DTO;

public class ActorDto
{
public int Id { get; set; }
public string Username { get; set; }
public string AvatarPath { get; set; }
}
16 changes: 16 additions & 0 deletions AskFm/AskFm.BLL/DTO/CreateNotificationRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AskFm.DAL.Enums;

namespace AskFm.BLL.DTO
{
public class CreateNotificationRequest
{
public int UserId { get; set; }
public NotificationStatus Type { get; set; }
public int ResourceId { get; set; }
public string Message { get; set; }
}
}
19 changes: 19 additions & 0 deletions AskFm/AskFm.BLL/DTO/NotificationDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using AskFm.BLL.DTO;

public class NotificationDto
{
public int Id { get; set; }
public string Type { get; set; }

public string Message { get; set; }

public bool IsRead { get; set; }
public DateTime CreatedAt { get; set; }

public int ResourceId { get; set; }
public int UserId { get; set; }
public ActorDto? Actor { get; set; }
public PaginationDto Pagination { get; set; }


}
Comment on lines +3 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing namespace; class is in the global namespace

This will break consumers expecting AskFm.BLL.DTO.NotificationDto.

Apply:

-public class NotificationDto
-{
+namespace AskFm.BLL.DTO;
+
+public class NotificationDto
+{

Also initialize non-nullable strings to satisfy nullability:

-    public string Type { get; set; }
+    public string Type { get; set; } = default!;
-    public string Message { get; set; }
+    public string Message { get; set; } = default!;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class NotificationDto
{
public int Id { get; set; }
public string Type { get; set; }
public string Message { get; set; }
public bool IsRead { get; set; }
public DateTime CreatedAt { get; set; }
public int ResourceId { get; set; }
public int UserId { get; set; }
public ActorDto? Actor { get; set; }
public PaginationDto Pagination { get; set; }
}
namespace AskFm.BLL.DTO;
public class NotificationDto
{
public int Id { get; set; }
public string Type { get; set; } = default!;
public string Message { get; set; } = default!;
public bool IsRead { get; set; }
public DateTime CreatedAt { get; set; }
public int ResourceId { get; set; }
public int UserId { get; set; }
public ActorDto? Actor { get; set; }
public PaginationDto Pagination { get; set; }
}
🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/DTO/NotificationDto.cs around lines 3 to 19, the class is
declared in the global namespace and non-nullable string properties are not
initialized; move the class into the AskFm.BLL.DTO namespace and initialize
non-nullable reference properties (at minimum Type and Message) with safe
defaults (e.g. empty strings). Also ensure any other non-nullable reference
properties (like Pagination) are either made nullable or given sensible default
instances so the DTO honors nullability annotations.

10 changes: 10 additions & 0 deletions AskFm/AskFm.BLL/DTO/PaginationDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace AskFm.BLL.DTO;

public class PaginationDto
{
public int CurrentPage { get; set; }
public int TotalPages { get; set; }
public int TotalCount { get; set; }
public bool HasNext { get; set; }
public bool HasPrevious { get; set; }
}
Loading
Loading