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
3 changes: 3 additions & 0 deletions backend/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,8 @@ dotnet_diagnostic.SA1206.severity = silent
# SA1602: Enumeration items should be documented
dotnet_diagnostic.SA1602.severity = silent

# SA1313: Parameter names should begin with lower-case letter
dotnet_diagnostic.SA1313.severity = silent

# XML comment analysis is disabled due to project configuration
dotnet_diagnostic.SA0001.severity = silent
3 changes: 2 additions & 1 deletion backend/ExternalApi/GamificationApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using TaskSync.Infrastructure.Http.Interface;
using TaskSync.Models.Dto;

using TaskStatus = TaskSync.Enums.TASK_STATUS;
using TaskStatus = TaskSync.Models.Enums.TASK_STATUS;

namespace TaskSync.ExternalApi
{
Expand All @@ -25,6 +25,7 @@ public async Task UpdatePoint(int taskId, TaskStatus status)
{
var httpMessage = new HttpRequestMessage(HttpMethod.Post, "points")
{
// Serialize Object -> JSON
Content = JsonContent.Create(new CreatePointDto()
{
TaskId = taskId,
Expand Down
2 changes: 1 addition & 1 deletion backend/ExternalApi/Interfaces/IGamificationApi.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using TaskStatus = TaskSync.Enums.TASK_STATUS;
using TaskStatus = TaskSync.Models.Enums.TASK_STATUS;

namespace TaskSync.ExternalApi.Interfaces
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
using TaskSync.Infrastructure.Caching.Interfaces;
using TaskSync.Infrastructure.Http;
using TaskSync.Infrastructure.Http.Interface;
using TaskSync.Infrastructure.Messaging;
using TaskSync.Infrastructure.Messaging.Interfaces;
using TaskSync.Infrastructure.Settings;
using TaskSync.Infrastructure.SignalR;
using TaskSync.Infrastructure.SignalR.Interfaces;
using TaskSync.Repositories;
using TaskSync.Repositories.Entities;
using TaskSync.Repositories.Interfaces;
using TaskSync.Services;
using TaskSync.Services.Interfaces;
using TaskSync.SignalR;
using TaskSync.SignalR.Interfaces;

namespace TaskSync.Infrastructure.Configurations
{
Expand All @@ -34,6 +36,7 @@ public static IServiceCollection ConfigureDependencyInjection(this IServiceColle
services.AddSingleton<ICacheBackgroundRefresher, CacheBackgroundRefresher>();
services.AddSingleton<IMemoryCacheService<IList<TaskEntity>>, TaskEntityCache>();
services.AddSingleton<IMemoryCacheService<ProjectEntity>, ProjectEntityCache>();
services.AddSingleton<IPointsEventPublisher, PointsEventPublisher>();

services.AddScoped<IHttpContextReader, HttpContextReader>();
services.AddScoped<IRepository<UserEntity>, UserRepository>();
Expand All @@ -44,7 +47,6 @@ public static IServiceCollection ConfigureDependencyInjection(this IServiceColle
services.AddScoped<IAuthenticationService, AuthenticationService>();
services.AddScoped<ITaskService, TaskService>();
services.AddScoped<ICommentService, CommentService>();

services.AddScoped<IGamificationApi, GamificationApi>();

return services;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public static IServiceCollection ConfigureAppSettings(this IServiceCollection se
{
services.Configure<AppInfo>(configuration.GetSection("AppInfo"));
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));
services.Configure<RabbitMqSettings>(configuration.GetSection("RabbitMq"));
services.Configure<FrontendSettings>(configuration.GetSection("Frontend"));
services.Configure<PostgreSqlSettings>(configuration.GetSection("PostgreSql"));
services.Configure<OtelCollectorSettings>(configuration.GetSection("OtelCollector"));
Expand Down
13 changes: 13 additions & 0 deletions backend/Infrastructure/Messaging/Contracts/PointAwardedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using TaskSync.Models.Enums;

namespace TaskSync.Infrastructure.Messaging.Contracts
{
public record PointAwardedEvent(
Guid EventId, // EventId is important for idempotency (dedupe on consumer side)
int? UserId,
int TaskId,
TASK_STATUS TaskStatus,
DateTimeOffset OccurredAtUtc,
string Source
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using TaskSync.Models.Enums;

namespace TaskSync.Infrastructure.Messaging.Interfaces
{
public interface IPointsEventPublisher
{
Task PublishPointAwardedAsync(int taskId, TASK_STATUS status, int? userId, CancellationToken ct = default);
}
}
86 changes: 86 additions & 0 deletions backend/Infrastructure/Messaging/PointsEventPublisher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Text;
using System.Text.Json;

using Microsoft.Extensions.Options;

using RabbitMQ.Client;

using TaskSync.Infrastructure.Messaging.Contracts;
using TaskSync.Infrastructure.Messaging.Interfaces;
using TaskSync.Infrastructure.Settings;
using TaskSync.Models.Enums;

namespace TaskSync.Infrastructure.Messaging
{
// sealed for better performance, since it won't be inherited by other classes
// used with service, repo, api, infra, any class that need no inheritance
public sealed class PointsEventPublisher : IPointsEventPublisher, IAsyncDisposable
{
private readonly ConnectionFactory _factory;
private IConnection? _connection;

public PointsEventPublisher(IOptions<RabbitMqSettings> options)
{
var s = options.Value;
_factory = new ConnectionFactory
{
HostName = s.Host,
Port = s.Port,
UserName = s.Username,
Password = s.Password,
VirtualHost = s.VirtualHost,
};
}

// RabbitMQ Overview: https://www.youtube.com/watch?v=deG25y_r6OY
// Producer(.NET) -> Exchange(points.award.x) -> Queue(points.award.q) -> Consumer(NestJs)
public async Task PublishPointAwardedAsync(int taskId, TASK_STATUS status, int? userId, CancellationToken ct = default)
{
var connection = await GetConnectionAsync(ct);
await using var channel = await connection.CreateChannelAsync();

var evt = new PointAwardedEvent(
EventId: Guid.NewGuid(),
UserId: userId,
TaskId: taskId,
TaskStatus: status,
OccurredAtUtc: DateTimeOffset.UtcNow,
Source: "tasksync-backend"
);
var body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(evt));
var props = new BasicProperties
{
Persistent = true,
ContentType = "application/json",
};

await channel.BasicPublishAsync(
exchange: RabbitMqTopologyInitializer.MainExchange,
routingKey: RabbitMqTopologyInitializer.MainRoutingKey,
mandatory: false,
basicProperties: props,
body: body,
cancellationToken: ct
);
Console.WriteLine($"[PointsEventPublisher] Published -> {RabbitMqTopologyInitializer.MainExchange} rk={RabbitMqTopologyInitializer.MainRoutingKey}: userId={evt.UserId}, taskId={evt.TaskId}, status={evt.TaskStatus}");
}

public async ValueTask DisposeAsync()
{
if (_connection != null)
{
await _connection.DisposeAsync();
}
}

private async Task<IConnection> GetConnectionAsync(CancellationToken ct)
{
if (_connection != null)
{
return _connection;
}
_connection = await _factory.CreateConnectionAsync(ct);
return _connection;
}
}
}
64 changes: 64 additions & 0 deletions backend/Infrastructure/Messaging/RabbitMqTopologyInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Microsoft.Extensions.Options;

using RabbitMQ.Client;

using TaskSync.Infrastructure.Settings;

namespace TaskSync.Infrastructure.Messaging
{
public sealed class RabbitMqTopologyInitializer : IHostedService
{
// Topology names (single source of truth)
public static string MainExchange = "points.award.x";
public static string MainQueue = "points.award.q";
public static string MainRoutingKey = "points.award";
public static string DeadLetterExchange = "points.award.dlx";
public static string DeadLetterQueue = "points.award.dlq";
public static string DeadLetterRoutingKey = "points.award.dlq";

private readonly RabbitMqSettings _settings;

public RabbitMqTopologyInitializer(IOptions<RabbitMqSettings> options)
{
_settings = options.Value;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
var factory = new ConnectionFactory
{
HostName = _settings.Host,
Port = _settings.Port,
UserName = _settings.Username,
Password = _settings.Password,
VirtualHost = _settings.VirtualHost,
};

await using var connection = await factory.CreateConnectionAsync(cancellationToken);
await using var channel = await connection.CreateChannelAsync();

// 1) Exchanges
await channel.ExchangeDeclareAsync(MainExchange, ExchangeType.Direct, durable: true, autoDelete: false, cancellationToken: cancellationToken);
await channel.ExchangeDeclareAsync(DeadLetterExchange, ExchangeType.Direct, durable: true, autoDelete: false, cancellationToken: cancellationToken);

// 2) DLQ + bind
await channel.QueueDeclareAsync(DeadLetterQueue, durable: true, exclusive: false, autoDelete: false, arguments: null, cancellationToken: cancellationToken);
await channel.QueueBindAsync(DeadLetterQueue, DeadLetterExchange, DeadLetterRoutingKey, cancellationToken: cancellationToken);

// 3) Main queue with DLQ args + bind
var args = new Dictionary<string, object>
{
["x-dead-letter-exchange"] = DeadLetterExchange,
["x-dead-letter-routing-key"] = DeadLetterRoutingKey,
};

await channel.QueueDeclareAsync(MainQueue, durable: true, exclusive: false, autoDelete: false, arguments: args, cancellationToken: cancellationToken);

Check warning on line 55 in backend/Infrastructure/Messaging/RabbitMqTopologyInitializer.cs

View workflow job for this annotation

GitHub Actions / 🧪 Build Backend

Argument of type 'Dictionary<string, object>' cannot be used for parameter 'arguments' of type 'IDictionary<string, object?>' in 'Task<QueueDeclareOk> IChannel.QueueDeclareAsync(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary<string, object?>? arguments = null, bool passive = false, bool noWait = false, CancellationToken cancellationToken = default(CancellationToken))' due to differences in the nullability of reference types.

Check warning on line 55 in backend/Infrastructure/Messaging/RabbitMqTopologyInitializer.cs

View workflow job for this annotation

GitHub Actions / 🧪 Build Backend

Argument of type 'Dictionary<string, object>' cannot be used for parameter 'arguments' of type 'IDictionary<string, object?>' in 'Task<QueueDeclareOk> IChannel.QueueDeclareAsync(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary<string, object?>? arguments = null, bool passive = false, bool noWait = false, CancellationToken cancellationToken = default(CancellationToken))' due to differences in the nullability of reference types.

Check warning on line 55 in backend/Infrastructure/Messaging/RabbitMqTopologyInitializer.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Backend Tests

Argument of type 'Dictionary<string, object>' cannot be used for parameter 'arguments' of type 'IDictionary<string, object?>' in 'Task<QueueDeclareOk> IChannel.QueueDeclareAsync(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary<string, object?>? arguments = null, bool passive = false, bool noWait = false, CancellationToken cancellationToken = default(CancellationToken))' due to differences in the nullability of reference types.
await channel.QueueBindAsync(MainQueue, MainExchange, MainRoutingKey, cancellationToken: cancellationToken);
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}
11 changes: 11 additions & 0 deletions backend/Infrastructure/Settings/RabbitMqSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace TaskSync.Infrastructure.Settings
{
public class RabbitMqSettings
{
public string Host { get; set; } = "localhost";
public int Port { get; set; } = 5672;
public string Username { get; set; } = "user";
public string Password { get; set; } = "password";
public string VirtualHost { get; set; } = "/";
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using TaskSync.Models.Dto;

namespace TaskSync.SignalR.Interfaces
namespace TaskSync.Infrastructure.SignalR.Interfaces
{
public interface ITaskNotificationService
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.SignalR;

namespace TaskSync.SignalR
namespace TaskSync.Infrastructure.SignalR
{
public class TaskHub : Hub
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Microsoft.AspNetCore.SignalR;

using TaskSync.Enums;
using TaskSync.Infrastructure.SignalR.Interfaces;
using TaskSync.Models.Dto;
using TaskSync.SignalR.Interfaces;
using TaskSync.Models.Enums;

namespace TaskSync.SignalR
namespace TaskSync.Infrastructure.SignalR
{
public class TaskNotificationService : ITaskNotificationService
{
Expand Down
3 changes: 2 additions & 1 deletion backend/MiddleWares/ExceptionHandlingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public async Task InvokeAsync(HttpContext context)
catch (Exception ex)
{
await HandleExceptionResponseAsync(context, ex);
_logger.LogError(ex, ex.Message);
_logger.LogError(ex, ex.Message); // todo-moch: fix why it does not show on console
Console.WriteLine($"Exception: {ex.Message} StackTrace: {ex.StackTrace}");
}
}

Expand Down
2 changes: 1 addition & 1 deletion backend/Models/Dto/CreatePointDto.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using TaskSync.Enums;
using TaskSync.Models.Enums;

namespace TaskSync.Models.Dto
{
Expand Down
2 changes: 1 addition & 1 deletion backend/Models/Dto/NotifyTask.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using TaskSync.Enums;
using TaskSync.Models.Enums;

namespace TaskSync.Models.Dto
{
Expand Down
2 changes: 1 addition & 1 deletion backend/Models/Dto/TaskDto.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using TaskSync.Enums;
using TaskSync.Models.Enums;

namespace TaskSync.Models.Dto
{
Expand Down
2 changes: 1 addition & 1 deletion backend/Models/Dto/UpdateTaskRequest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;

using TaskSync.Enums;
using TaskSync.Models.Enums;

public class UpdateTaskRequest : IValidatableObject
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace TaskSync.Enums
namespace TaskSync.Models.Enums
{
public enum NOTIFY_STATUS
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace TaskSync.Enums
namespace TaskSync.Models.Enums
{
public enum TASK_COMMENT_TYPE
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace TaskSync.Enums
namespace TaskSync.Models.Enums
{
public enum TASK_STATUS
{
Expand Down
4 changes: 3 additions & 1 deletion backend/Program.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using System.Text.Json.Serialization;

using TaskSync.Infrastructure.Configurations;
using TaskSync.Infrastructure.Messaging;
using TaskSync.Infrastructure.SignalR;
using TaskSync.MiddleWares;
using TaskSync.SignalR;

// ------------------------ Setup all services ----------------------------------
var builder = WebApplication.CreateBuilder(args);
Expand All @@ -22,6 +23,7 @@
builder.Services.ConfigureApiVersion();
builder.Services.ConfigureCors();
builder.Services.ConfigureJwt();
builder.Services.AddHostedService<RabbitMqTopologyInitializer>();
if (builder.Environment.IsDevelopment())
{
builder.ConfigureTelemetry();
Expand Down
2 changes: 1 addition & 1 deletion backend/Repositories/Entities/TaskCommentEntity.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations.Schema;

using TaskSync.Enums;
using TaskSync.Models.Enums;

namespace TaskSync.Repositories.Entities
{
Expand Down
2 changes: 1 addition & 1 deletion backend/Repositories/Entities/TaskEntity.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations.Schema;

using TaskSync.Enums;
using TaskSync.Models.Enums;

namespace TaskSync.Repositories.Entities
{
Expand Down
2 changes: 1 addition & 1 deletion backend/Services/AuthenticationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public AuthenticationService(IJwtService jwtService, IRepository<UserEntity> use
{
var user = await _userRepository.GetAsync(request.Email);

// string passwordHash = BCrypt.Net.BCrypt.HashPassword("mypassword");
// string passwordHash = BCrypt.Net.BCrypt.HashPassword("mypassword"); generate hash password
if (user == null || !BCrypt.Net.BCrypt.Verify(request.Password, user.Password))
{
return null;
Expand Down
Loading
Loading