Skip to content

Create Gamification API (part2) #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 24, 2025
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
13 changes: 0 additions & 13 deletions backend/Controllers/AuthenticationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

using TaskSync.Filters;
using TaskSync.Models.Dto;
using TaskSync.Services.Interfaces;

Expand All @@ -23,18 +22,6 @@ public AuthenticationController(IUserService userService, IAuthenticationService
_authenticationService = authenticationService;
}

[ValidateRequestFilter]
[HttpPost("test")]
public async Task<IActionResult> Test([FromBody] TestRequest request)
{
Console.WriteLine("In 'auth/test' controller");

var result = await _userService.GetUserAsync();

return Ok(result);
}

[ValidateRequestFilter]
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
Expand Down
20 changes: 20 additions & 0 deletions backend/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Mvc;

using TaskSync.Services.Interfaces;

namespace TaskSync.Controllers
{
[Route("")]
public class HomeController : Controller
{
private readonly IHomeVmService _homeVmService;
public HomeController(IHomeVmService homeVmService) => _homeVmService = homeVmService;

[HttpGet("")]
public IActionResult Index()
{
var vm = _homeVmService.Build();
return View("~/Views/Home/Index.cshtml", vm); // Return Razor View
}
}
}
25 changes: 14 additions & 11 deletions backend/Controllers/TaskController.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

using TaskSync.Controllers.TempDto;
using TaskSync.Models;
using TaskSync.Models.Dto;
using TaskSync.Services.Interfaces;

namespace TaskSync.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/task")]
[Route("api/v{version:apiVersion}")]
public class TaskController : ControllerBase
{
private readonly ITaskService _taskService;
Expand All @@ -18,31 +19,33 @@ public TaskController(ITaskService taskService)
_taskService = taskService;
}

[HttpGet("getTasks/{projectId}")]
[HttpGet("projects/{projectId}/tasks")]
public async Task<IActionResult> GetTasks([FromRoute] int projectId)
{
var tasks = await _taskService.GetTasksAsync(projectId);

return Ok(new KanbanBoardVm() { Tasks = tasks });
}

[HttpPost("addTask")]
public async Task<IActionResult> AddTask([FromBody] AddTaskRequest request)
[Authorize]
[HttpPost("projects/{projectId}/tasks")]
public async Task<IActionResult> AddTask([FromRoute] int projectId, [FromBody] AddTaskRequest request)
{
var taskDto = await _taskService.AddTaskAsync(request);
var taskDto = await _taskService.AddTaskAsync(projectId, request);
return StatusCode(StatusCodes.Status201Created, taskDto);
}

[HttpPatch("updateStatus/{taskId}")]
[Authorize]
[HttpPatch("tasks/{taskId}")]
public async Task<IActionResult> UpdateStatus([FromRoute] int taskId, [FromBody] UpdateTaskRequest request) // [FromRoute], [FromBody] are called ModelBining
{
var taskDto = await _taskService.UpdateTaskStatusAsync(taskId, request);

return taskDto != null ? Ok(taskDto) : BadRequest("Task not found");
}

[HttpDelete("deleteTask/{taskId}")]
public async Task<IActionResult> AddTask([FromRoute] int taskId)
[Authorize]
[HttpDelete("tasks/{taskId}")]
public async Task<IActionResult> DeleteTask([FromRoute] int taskId)
{
var isSucess = await _taskService.DeleteTaskAsync(taskId);
return isSucess ? NoContent() : NotFound("Task not found");
Expand Down
2 changes: 2 additions & 0 deletions backend/Enums/TaskStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ public enum TASK_STATUS
TODO,
INPROGRESS,
DONE,
CREATE,
DELETE,
}
}
50 changes: 50 additions & 0 deletions backend/ExternalApi/GamificationApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Microsoft.Extensions.Options;

using TaskSync.ExternalApi.Interfaces;
using TaskSync.Infrastructure.Http.Interface;
using TaskSync.Infrastructure.Settings;
using TaskSync.Models.Dto;

using TaskStatus = TaskSync.Enums.TASK_STATUS;

namespace TaskSync.ExternalApi
{
public class GamificationApi : IGamificationApi
{
private readonly GamificationApiSettings _gamApiSettings;
private readonly IHttpContextReader _httpContextReader;
private readonly HttpClient _httpClient;
private readonly ILogger<GamificationApi> _logger;

public GamificationApi(IOptions<GamificationApiSettings> options, IHttpContextReader httpContextReader, IHttpClientFactory httpClientFactory, ILogger<GamificationApi> logger)
{
_gamApiSettings = options.Value;
_httpContextReader = httpContextReader;
_httpClient = httpClientFactory.CreateClient();
_logger = logger;
}

public async Task UpdatePoint(int taskId, TaskStatus status)
{
try
{
var httpMessage = new HttpRequestMessage(HttpMethod.Post, $"{_gamApiSettings.BaseUrl}/points")
{
Content = JsonContent.Create(new CreatePointDto()
{
TaskId = taskId,
TaskStatus = status,
UserId = _httpContextReader.GetUserId(),
}),
};
httpMessage.Headers.Add("x-gamapi-auth", "true");

await _httpClient.SendAsync(httpMessage);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
}
}
}
9 changes: 9 additions & 0 deletions backend/ExternalApi/Interfaces/IGamificationApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using TaskStatus = TaskSync.Enums.TASK_STATUS;

namespace TaskSync.ExternalApi.Interfaces
{
public interface IGamificationApi
{
Task UpdatePoint(int taskId, TaskStatus status);
}
}
2 changes: 1 addition & 1 deletion backend/Filters/ValidateRequestFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace TaskSync.Filters
{
// todo-moch: 1) add some custom logic
// todo-moch: add some custom logic
public class ValidateRequestFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
Expand Down
15 changes: 0 additions & 15 deletions backend/GamificationApi.cs

This file was deleted.

10 changes: 5 additions & 5 deletions backend/Infrastructure/Caching/MemoryCacheBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ protected MemoryCacheBase(IMemoryCache memoryCache)
_memoryCache = memoryCache;
}

protected abstract string GetCacheKey(int cacheKey);

public async Task<T?> GetAsync(int cacheKey, Func<Task<T?>> fallbackCall)
{
if (_memoryCache.TryGetValue(GetCacheKey(cacheKey), out T? cached))
{
Console.WriteLine($"[MemoryCache] get '{GetCacheKey(cacheKey)}' from memory");
Console.WriteLine($"[MemoryCacheBase] get '{GetCacheKey(cacheKey)}' from memory");
return cached;
}

Expand All @@ -27,7 +25,7 @@ protected MemoryCacheBase(IMemoryCache memoryCache)
{
Set(cacheKey, data);
}
Console.WriteLine($"[MemoryCache] get '{GetCacheKey(cacheKey)}' from database");
Console.WriteLine($"[MemoryCacheBase] get '{GetCacheKey(cacheKey)}' from database");

return data;
}
Expand All @@ -47,7 +45,9 @@ public void Set(int cacheKey, T data)
public void Remove(int cacheKey)
{
_memoryCache.Remove(GetCacheKey(cacheKey));
Console.WriteLine($"[MemoryCache] remove '{GetCacheKey(cacheKey)}' from memory");
Console.WriteLine($"[MemoryCacheBase] remove '{GetCacheKey(cacheKey)}' from memory");
}

protected abstract string GetCacheKey(int cacheKey);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Microsoft.EntityFrameworkCore;

using TaskSync.ExternalApi;
using TaskSync.ExternalApi.Interfaces;
using TaskSync.Infrastructure.Caching;
using TaskSync.Infrastructure.Caching.Interfaces;
using TaskSync.Infrastructure.Http;
Expand All @@ -16,16 +18,17 @@ namespace TaskSync.Infrastructure.Configurations
{
public static class DependencyInjectionConfiguration
{
public static IServiceCollection RegisterDependencies(this IServiceCollection services, IConfiguration configuration)
public static IServiceCollection ConfigureDependencyInjection(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString("DefaultConnection"))
);

services.AddSingleton<IJwtService, JwtService>();
services.AddSingleton<IHomeVmService, HomeVmService>();
services.AddSingleton<ITaskNotificationService, TaskNotificationService>();
services.AddSingleton<IMemoryCacheService<IList<TaskEntity>>, TaskEntitiesCache>();
services.AddSingleton<ICacheBackgroundRefresher, CacheBackgroundRefresher>();
services.AddSingleton<IMemoryCacheService<IList<TaskEntity>>, TaskEntitiesCache>();

services.AddScoped<IHttpContextReader, HttpContextReader>();
services.AddScoped<IRepository<UserEntity>, UserRepository>();
Expand All @@ -34,6 +37,8 @@ public static IServiceCollection RegisterDependencies(this IServiceCollection se
services.AddScoped<IAuthenticationService, AuthenticationService>();
services.AddScoped<ITaskService, TaskService>();

services.AddScoped<IGamificationApi, GamificationApi>();

return services;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.IO.Compression;

using Microsoft.AspNetCore.ResponseCompression;

namespace TaskSync.Infrastructure.Configurations
{
public static class ResponseCompressionConfiguration
{

Check warning on line 8 in backend/Infrastructure/Configurations/ResponseCompressionConfiguration.cs

View workflow job for this annotation

GitHub Actions / 🧪 Build Backend

An opening brace should not be followed by a blank line. (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1505.md)

Check warning on line 8 in backend/Infrastructure/Configurations/ResponseCompressionConfiguration.cs

View workflow job for this annotation

GitHub Actions / 🧪 Build Backend

An opening brace should not be followed by a blank line. (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1505.md)

Check warning on line 8 in backend/Infrastructure/Configurations/ResponseCompressionConfiguration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Backend Tests

An opening brace should not be followed by a blank line. (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1505.md)

public static IServiceCollection ConfigureResponseCompression(this IServiceCollection services)
{
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/json" }); // explicitly add JSON
});

services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});

return services;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ public static class SettingsConfiguration
{
public static IServiceCollection ConfigureAppSettings(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<AppInfo>(configuration.GetSection("AppInfo"));
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));
services.Configure<MiddlewareSettings>(configuration.GetSection("MiddlewareSettings"));
services.Configure<GamificationApiSettings>(configuration.GetSection("GamificationApi"));

return services;
}
Expand Down
22 changes: 16 additions & 6 deletions backend/Infrastructure/Http/HttpContextReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,30 @@
public class HttpContextReader : IHttpContextReader
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextReader(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;

public HttpContextReader(IHttpContextAccessor httpContextAccessor)
public string? GetConnectionId()
{
_httpContextAccessor = httpContextAccessor;
return _httpContextAccessor.HttpContext?.Request.Headers["x-connection-id"].FirstOrDefault();
}

public string? GetConnectionId()
public string GetUserId()
{
return _httpContextAccessor.HttpContext?.Request.Headers["x-connection-id"].FirstOrDefault();
return _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty;
}

public string? GetUserId()
public string? GetCookie(string cookieKey)
{
return _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
return _httpContextAccessor.HttpContext?.Request.Cookies[cookieKey]; // read
// Response.Cookies.Append("MyCookieKey", "my_value", new CookieOptions // write
// {
// HttpOnly = true, // javascript cannot access
// Secure = true, // only https
// SameSite = SameSiteMode.Strict,
// Expires = DateTimeOffset.UtcNow.AddDays(7)
// });
//

Check warning on line 32 in backend/Infrastructure/Http/HttpContextReader.cs

View workflow job for this annotation

GitHub Actions / 🧪 Build Backend

Check warning on line 32 in backend/Infrastructure/Http/HttpContextReader.cs

View workflow job for this annotation

GitHub Actions / 🧪 Build Backend

Check warning on line 32 in backend/Infrastructure/Http/HttpContextReader.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Backend Tests

// Response.Cookies.Delete("MyCookieKey"); // delete
}
}
}
3 changes: 2 additions & 1 deletion backend/Infrastructure/Http/Interface/IHttpContextReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public interface IHttpContextReader
{
string? GetConnectionId();
string? GetUserId();
string GetUserId();
string? GetCookie(string cookieKey);
}
}
8 changes: 8 additions & 0 deletions backend/Infrastructure/Settings/AppInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TaskSync.Infrastructure.Settings
{
public class AppInfo
{
public required string AppName { get; set; }
public required string Environment { get; set; }
}
}
7 changes: 7 additions & 0 deletions backend/Infrastructure/Settings/GamificationApiSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TaskSync.Infrastructure.Settings
{
public class GamificationApiSettings
{
public required string BaseUrl { get; set; }
}
}
Loading
Loading