Add description
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing

This commit is contained in:
Kasper Juul Hermansen 2021-11-18 16:53:21 +01:00
parent afb3bfd681
commit c281d5a6a7
Signed by: kjuulh
GPG Key ID: DCD9397082D97069
39 changed files with 639 additions and 416 deletions

View File

@ -1,66 +1,70 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Todo.Core.Interfaces.Persistence; using Todo.Core.Interfaces.Persistence;
namespace Todo.Api.Controllers namespace Todo.Api.Controllers;
[ApiController]
[Route("api/auth")]
[Authorize]
[AllowAnonymous]
public class AuthController : ControllerBase
{ {
[ApiController] private readonly IUserRepository _userRepository;
[Route("api/auth")]
[Authorize] public AuthController(IUserRepository userRepository)
[AllowAnonymous]
public class AuthController : ControllerBase
{ {
private readonly IUserRepository _userRepository; _userRepository = userRepository;
}
public AuthController(IUserRepository userRepository) [HttpPost("register")]
{ public async Task<IActionResult> Register(
_userRepository = userRepository; [FromBody] RegisterUserRequest request)
} {
var user = await _userRepository.Register(
request.Email,
request.Password);
[HttpPost("register")] return Ok(user);
public async Task<IActionResult> Register([FromBody] RegisterUserRequest request) }
{
var user = await _userRepository.Register(request.Email, request.Password);
return Ok(user); [HttpGet("login")]
} public async Task<IActionResult> Login([FromQuery] string returnUrl)
{
[HttpGet("login")] var props = new AuthenticationProperties
public async Task<IActionResult> Login([FromQuery] string returnUrl)
{ {
var props = new AuthenticationProperties RedirectUri = Url.Action(nameof(Callback)),
Items =
{ {
RedirectUri = Url.Action(nameof(Callback)),
Items =
{ {
{"returnUrl", returnUrl} "returnUrl", returnUrl
} }
};
return Challenge(props);
}
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result =
await HttpContext.AuthenticateAsync("oidc");
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
} }
};
return Challenge(props);
}
var returnUrl = result.Properties?.Items["returnUrl"] ?? "~/"; [HttpGet]
return Redirect(returnUrl); public async Task<IActionResult> Callback()
} {
// read external identity from the temporary cookie
var result =
await HttpContext.AuthenticateAsync("oidc");
if (result?.Succeeded != true)
throw new Exception("External authentication error");
public record RegisterUserRequest var returnUrl = result.Properties?.Items["returnUrl"] ?? "~/";
{ return Redirect(returnUrl);
[Required] public string Email { get; init; } }
[Required] public string Password { get; init; }
} public record RegisterUserRequest
{
[Required]
public string Email { get; init; }
[Required]
public string Password { get; init; }
} }
} }

View File

@ -19,25 +19,37 @@ public class TodosController : ControllerBase
[HttpPost] [HttpPost]
[Authorize] [Authorize]
public async Task<ActionResult<Core.Entities.Todo>> CreateTodo([FromBody] CreateTodoRequest request) public async Task<ActionResult<Core.Entities.Todo>> CreateTodo(
[FromBody] CreateTodoRequest request)
{ {
var userId = User.FindFirstValue("sub") ?? var userId = User.FindFirstValue("sub") ??
throw new InvalidOperationException("Could not get user, something has gone terribly wrong"); throw new InvalidOperationException("Could not get user, something has gone terribly wrong");
return Ok(await _todoRepository.CreateTodoAsync(request.Title, String.Empty, userId)); return Ok(
await _todoRepository.CreateTodoAsync(
request.Title,
string.Empty,
"",
userId));
} }
[HttpGet] [HttpGet]
[Authorize] [Authorize]
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetTodos() => public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetTodos()
Ok(await _todoRepository.GetTodosAsync()); {
return Ok(await _todoRepository.GetTodosAsync());
}
[HttpGet("not-done")] [HttpGet("not-done")]
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetNotDoneTodos() => public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>>
Ok(await _todoRepository.GetNotDoneTodos()); GetNotDoneTodos()
{
return Ok(await _todoRepository.GetNotDoneTodos());
}
public record CreateTodoRequest public record CreateTodoRequest
{ {
[Required] public string Title { get; init; } [Required]
public string Title { get; init; }
} }
} }

View File

@ -5,14 +5,17 @@ namespace Todo.Api.Hubs.Models;
public record TodoResponse public record TodoResponse
{ {
[JsonPropertyName("id")] [JsonPropertyName("id")]
public string Id { get; init; } public string Id { get; init; }
[JsonPropertyName("title")] [JsonPropertyName("title")]
public string Title { get; init; } public string Title { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("status")] [JsonPropertyName("status")]
public bool Status { get; init; } public bool Status { get; init; }
[JsonPropertyName("project")] [JsonPropertyName("project")]
public string Project { get; set; } public string? Project { get; init; }
} }

View File

@ -5,14 +5,17 @@ namespace Todo.Api.Hubs.Models;
public record UpdateTodoRequest public record UpdateTodoRequest
{ {
[JsonPropertyName("id")] [JsonPropertyName("id")]
public string Id { get; init; } public string Id { get; init; }
[JsonPropertyName("title")] [JsonPropertyName("title")]
public string Title { get; init; } public string Title { get; init; }
[JsonPropertyName("status")] [JsonPropertyName("status")]
public bool Status { get; init; } public bool Status { get; init; }
[JsonPropertyName("project")] [JsonPropertyName("project")]
public string Project { get; set; } public string? Project { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
} }

View File

@ -1,5 +1,3 @@
using System.Collections.Concurrent;
using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -9,17 +7,28 @@ using Todo.Core.Application.Commands.Todo;
using Todo.Core.Application.Services.UserConnectionStore; using Todo.Core.Application.Services.UserConnectionStore;
using Todo.Core.Interfaces.Persistence; using Todo.Core.Interfaces.Persistence;
using Todo.Core.Interfaces.User; using Todo.Core.Interfaces.User;
using Todo.Infrastructure;
namespace Todo.Api.Hubs; namespace Todo.Api.Hubs;
[Authorize] [Authorize]
public class TodoHub : Hub public class TodoHub : Hub
{ {
private readonly ITodoRepository _todoRepository;
private readonly IUserConnectionStore _userConnectionStore;
private readonly ICurrentUserService _currentUserService; private readonly ICurrentUserService _currentUserService;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly ITodoRepository _todoRepository;
private readonly IUserConnectionStore _userConnectionStore;
public TodoHub(
ITodoRepository todoRepository,
IUserConnectionStore userConnectionStore,
ICurrentUserService currentUserService,
IMediator mediator)
{
_todoRepository = todoRepository;
_userConnectionStore = userConnectionStore;
_currentUserService = currentUserService;
_mediator = mediator;
}
public override Task OnConnectedAsync() public override Task OnConnectedAsync()
{ {
@ -38,19 +47,10 @@ public class TodoHub : Hub
return base.OnDisconnectedAsync(exception); return base.OnDisconnectedAsync(exception);
} }
public TodoHub( public async Task CreateTodo(
ITodoRepository todoRepository, string todoTitle,
IUserConnectionStore userConnectionStore, string? projectName,
ICurrentUserService currentUserService, string? description)
IMediator mediator)
{
_todoRepository = todoRepository;
_userConnectionStore = userConnectionStore;
_currentUserService = currentUserService;
_mediator = mediator;
}
public async Task CreateTodo(string todoTitle, string? projectName)
{ {
if (todoTitle is null) if (todoTitle is null)
throw new ArgumentException("title cannot be null"); throw new ArgumentException("title cannot be null");
@ -58,7 +58,11 @@ public class TodoHub : Hub
//var userId = GetUserId(); //var userId = GetUserId();
//var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName, userId); //var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName, userId);
await _mediator.Send(new CreateTodoCommand(todoTitle, projectName)); await _mediator.Send(
new CreateTodoCommand(
todoTitle,
projectName,
description));
await GetInboxTodos(); await GetInboxTodos();
} }
@ -67,7 +71,10 @@ public class TodoHub : Hub
public async Task UpdateTodo(string todoId, bool todoStatus) public async Task UpdateTodo(string todoId, bool todoStatus)
{ {
var userId = GetUserId(); var userId = GetUserId();
await _todoRepository.UpdateTodoStatus(todoId, todoStatus, userId); await _todoRepository.UpdateTodoStatus(
todoId,
todoStatus,
userId);
await GetInboxTodos(); await GetInboxTodos();
} }
@ -75,78 +82,115 @@ public class TodoHub : Hub
public async Task GetTodos() public async Task GetTodos()
{ {
var todos = await _todoRepository.GetTodosAsync(); var todos = await _todoRepository.GetTodosAsync();
var serializedTodos = JsonSerializer.Serialize(todos var serializedTodos = JsonSerializer.Serialize(
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.Project }) todos
.ToList()); .Select(
t => new TodoResponse
{
Id = t.Id,
Title = t.Title,
Status = t.Status,
Project = t.Project,
Description = t.Description
})
.ToList());
await RunOnUserConnections(async (connections) => await RunOnUserConnections(
await Clients.Clients(connections).SendAsync("todos", serializedTodos)); async connections =>
await Clients.Clients(connections)
.SendAsync("todos", serializedTodos));
} }
public async Task GetInboxTodos() public async Task GetInboxTodos()
{ {
var todos = await _todoRepository.GetNotDoneTodos(); var todos = await _todoRepository.GetNotDoneTodos();
var serializedTodos = JsonSerializer.Serialize(todos var serializedTodos = JsonSerializer.Serialize(
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.Project }) todos
.ToList()); .Select(
t => new TodoResponse
{
Id = t.Id,
Title = t.Title,
Status = t.Status,
Project = t.Project,
Description = t.Description
})
.ToList());
await RunOnUserConnections(async (connections) => await RunOnUserConnections(
await Clients.Clients(connections).SendAsync("getInboxTodos", serializedTodos)); async connections =>
await Clients.Clients(connections)
.SendAsync("getInboxTodos", serializedTodos));
} }
public async Task GetTodo(string todoId) public async Task GetTodo(string todoId)
{ {
var todo = await _todoRepository.GetTodoByIdAsync(todoId); var todo = await _todoRepository.GetTodoByIdAsync(todoId);
var serializedTodo = JsonSerializer.Serialize(new TodoResponse() var serializedTodo = JsonSerializer.Serialize(
{ new TodoResponse
Id = todo.Id, {
Project = todo.Project, Id = todo.Id,
Status = todo.Status, Project = todo.Project,
Title = todo.Title, Status = todo.Status,
}); Title = todo.Title,
Description = todo.Description
});
await RunOnUserConnections(async (connections) => await RunOnUserConnections(
await Clients.Clients(connections).SendAsync("getTodo", serializedTodo)); async connections =>
await Clients.Clients(connections)
.SendAsync("getTodo", serializedTodo));
} }
public async Task ReplaceTodo(string updateTodoRequest) public async Task ReplaceTodo(string updateTodoRequest)
{ {
var updateTodo = JsonSerializer.Deserialize<UpdateTodoRequest>(updateTodoRequest); var updateTodo =
JsonSerializer.Deserialize<UpdateTodoRequest>(updateTodoRequest);
if (updateTodo is null) if (updateTodo is null)
throw new InvalidOperationException("Could not parse invalid updateTodo"); throw new InvalidOperationException("Could not parse invalid updateTodo");
var userId = GetUserId(); var userId = GetUserId();
var updatedTodo = await _todoRepository.UpdateTodoAsync(new Core.Entities.Todo() var updatedTodo = await _todoRepository.UpdateTodoAsync(
{ new Core.Entities.Todo
Id = updateTodo.Id, {
Project = updateTodo.Project, Id = updateTodo.Id,
Status = updateTodo.Status, Project = updateTodo.Project,
Title = updateTodo.Title, Status = updateTodo.Status,
AuthorId = userId Title = updateTodo.Title,
}); AuthorId = userId,
Description = updateTodo.Description
});
var serializedTodo = JsonSerializer.Serialize(new TodoResponse() var serializedTodo = JsonSerializer.Serialize(
{ new TodoResponse
Id = updatedTodo.Id, {
Project = updatedTodo.Project, Id = updatedTodo.Id,
Status = updatedTodo.Status, Project = updatedTodo.Project,
Title = updatedTodo.Title, Status = updatedTodo.Status,
}); Title = updatedTodo.Title,
Description = updatedTodo.Description
});
await RunOnUserConnections(async (connections) => await RunOnUserConnections(
await Clients.Clients(connections).SendAsync("getTodo", serializedTodo)); async connections =>
await Clients.Clients(connections)
.SendAsync("getTodo", serializedTodo));
} }
private async Task RunOnUserConnections(Func<IEnumerable<string>, Task> action) private async Task RunOnUserConnections(
Func<IEnumerable<string>, Task> action)
{ {
var userId = GetUserId(); var userId = GetUserId();
var connections = await _userConnectionStore.GetConnectionsAsync(userId); var connections =
await _userConnectionStore.GetConnectionsAsync(userId);
await action(connections); await action(connections);
} }
private string GetUserId() => private string GetUserId()
_currentUserService.GetUserId() ?? {
throw new InvalidOperationException("User id was invalid. Something has gone terribly wrong"); return _currentUserService.GetUserId() ??
throw new InvalidOperationException("User id was invalid. Something has gone terribly wrong");
}
} }

View File

@ -3,19 +3,20 @@ global using System.Collections.Generic;
global using System.Linq; global using System.Linq;
global using System.Threading.Tasks; global using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Todo.Api namespace Todo.Api;
public class Program
{ {
public class Program public static void Main(string[] args)
{ {
public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
CreateHostBuilder(args).Build().Run(); }
private static IHostBuilder CreateHostBuilder(string[] args) => private static IHostBuilder CreateHostBuilder(string[] args)
Host.CreateDefaultBuilder(args) {
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
} }
} }

View File

@ -23,7 +23,7 @@
"MONGODB__Host": "localhost", "MONGODB__Host": "localhost",
"MONGODB__Port": "27017", "MONGODB__Port": "27017",
"GITEA__Url": "https://git.front.kjuulh.io", "GITEA__Url": "https://git.front.kjuulh.io",
"GITEA__ClientId": "6982ef4f-cfc1-431c-a442-fad98355a059", "GITEA__ClientId": "6982ef4f-cfc1-431c-a442-fad98355a059",
"GITEA__ClientSecret": "hXUrUz5xPhC7IE3dQKft9lHboBEwhNC8yFjSzKgF9Nyr" "GITEA__ClientSecret": "hXUrUz5xPhC7IE3dQKft9lHboBEwhNC8yFjSzKgF9Nyr"
} }
} }

View File

@ -10,15 +10,15 @@ namespace Todo.Api.Publishers;
public class TodoPublisher : ITodoPublisher public class TodoPublisher : ITodoPublisher
{ {
private readonly IHubContext<TodoHub> _hubContext;
private readonly ICurrentUserService _currentUserService; private readonly ICurrentUserService _currentUserService;
private readonly IUserConnectionStore _userConnectionStore; private readonly IHubContext<TodoHub> _hubContext;
private readonly ILogger<TodoPublisher> _logger; private readonly ILogger<TodoPublisher> _logger;
private readonly IUserConnectionStore _userConnectionStore;
public TodoPublisher( public TodoPublisher(
IHubContext<TodoHub> hubContext, IHubContext<TodoHub> hubContext,
ICurrentUserService currentUserService, ICurrentUserService currentUserService,
IUserConnectionStore userConnectionStore, IUserConnectionStore userConnectionStore,
ILogger<TodoPublisher> logger) ILogger<TodoPublisher> logger)
{ {
_hubContext = hubContext; _hubContext = hubContext;
@ -26,17 +26,24 @@ public class TodoPublisher : ITodoPublisher
_userConnectionStore = userConnectionStore; _userConnectionStore = userConnectionStore;
_logger = logger; _logger = logger;
} }
public async Task Publish(string todoId, CancellationToken cancellationToken) public async Task Publish(
string todoId,
CancellationToken cancellationToken)
{ {
var userId = _currentUserService.GetUserId() ?? throw new InvalidOperationException("Cannot proceed without user"); var userId = _currentUserService.GetUserId() ??
var connections = await _userConnectionStore.GetConnectionsAsync(userId); throw new InvalidOperationException("Cannot proceed without user");
var connections =
await _hubContext await _userConnectionStore.GetConnectionsAsync(userId);
.Clients
.Clients(connections) await _hubContext
.SendAsync("todoCreated", todoId , cancellationToken); .Clients
.Clients(connections)
_logger.LogInformation("todo created {TodoId}", todoId); .SendAsync(
"todoCreated",
todoId,
cancellationToken);
_logger.LogInformation("todo created {TodoId}", todoId);
} }
} }

View File

@ -8,10 +8,14 @@ public class HttpContextCurrentUserService : ICurrentUserService
{ {
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextCurrentUserService(IHttpContextAccessor httpContextAccessor) public HttpContextCurrentUserService(
IHttpContextAccessor httpContextAccessor)
{ {
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
} }
public string? GetUserId() => _httpContextAccessor.HttpContext?.User.FindFirstValue("sub"); public string? GetUserId()
{
return _httpContextAccessor.HttpContext?.User.FindFirstValue("sub");
}
} }

View File

@ -10,112 +10,134 @@ using Microsoft.OpenApi.Models;
using Todo.Api.Hubs; using Todo.Api.Hubs;
using Todo.Api.Publishers; using Todo.Api.Publishers;
using Todo.Api.Services; using Todo.Api.Services;
using Todo.Core;
using Todo.Core.Interfaces.Publisher;
using Todo.Core.Interfaces.User; using Todo.Core.Interfaces.User;
using Todo.Infrastructure; using Todo.Infrastructure;
using Todo.Persistence; using Todo.Persistence;
using Todo.Persistence.Mongo; using Todo.Persistence.Mongo;
using Todo.Core;
using Todo.Core.Interfaces.Publisher;
namespace Todo.Api namespace Todo.Api;
public class Startup
{ {
public class Startup public Startup(IConfiguration configuration)
{ {
public Startup(IConfiguration configuration) Configuration = configuration;
{ }
Configuration = configuration;
}
public IConfiguration Configuration { get; } public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container. // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddControllers(); services.AddControllers();
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddCors(options => services.AddCors(
options =>
{ {
options.AddDefaultPolicy(builder => options.AddDefaultPolicy(
builder.WithOrigins("http://localhost:3000", "https://todo.front.kjuulh.io") builder =>
.AllowCredentials() builder.WithOrigins(
.AllowAnyHeader() "http://localhost:3000",
.AllowAnyMethod()); "https://todo.front.kjuulh.io")
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod());
}); });
services.AddCore(); services.AddCore();
services.AddScoped<ICurrentUserService, HttpContextCurrentUserService>(); services
services.AddScoped<ITodoPublisher, TodoPublisher>(); .AddScoped<ICurrentUserService, HttpContextCurrentUserService>();
services.AddScoped<ITodoPublisher, TodoPublisher>();
services.AddSwaggerGen(c => services.AddSwaggerGen(
c =>
{ {
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo.Api", Version = "v1" }); c.SwaggerDoc(
"v1",
new OpenApiInfo
{
Title = "Todo.Api",
Version = "v1"
});
}); });
JwtSecurityTokenHandler.DefaultMapInboundClaims = false; JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddInfrastructure(Configuration); services.AddInfrastructure(Configuration);
services.AddPersistence(Configuration, out var mongoDbOptions); services.AddPersistence(Configuration, out var mongoDbOptions);
services services
.AddHealthChecks() .AddHealthChecks()
.AddMongoDb(MongoDbConnectionHandler.FormatConnectionString(mongoDbOptions)); .AddMongoDb(
services.AddSignalR(); MongoDbConnectionHandler
.FormatConnectionString(mongoDbOptions));
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.MigrateMongoDb();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(
c => c.SwaggerEndpoint(
"/swagger/v1/swagger.json",
"Todo.Api v1"));
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. app.UseRouting();
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseCors();
{ app.UseInfrastructure();
app.MigrateMongoDb(); app.UseAuthentication();
app.UseAuthorization();
if (env.IsDevelopment()) app.UseEndpoints(
{ endpoints =>
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Todo.Api v1"));
}
app.UseRouting();
app.UseCors();
app.UseInfrastructure();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{ {
endpoints.MapControllers(); endpoints.MapControllers();
endpoints.MapHub<TodoHub>("/hubs/todo"); endpoints.MapHub<TodoHub>("/hubs/todo");
endpoints.MapHealthChecks("/health/live", new HealthCheckOptions() endpoints.MapHealthChecks(
{ "/health/live",
ResponseWriter = async (context, report) => new HealthCheckOptions
{ {
var response = new HealthCheckResponse() ResponseWriter = async (context, report) =>
{ {
Status = report.Status.ToString(), var response = new HealthCheckResponse
HealthChecks = report.Entries.Select(x => new IndividualHealthCheckResponse
{ {
Component = x.Key, Status = report.Status.ToString(),
Status = x.Value.Status.ToString(), HealthChecks = report.Entries.Select(
Description = x.Value.Description x => new IndividualHealthCheckResponse
}), {
HealthCheckDuration = report.TotalDuration Component = x.Key,
}; Status = x.Value.Status.ToString(),
await context.Response.WriteAsJsonAsync(response); Description = x.Value.Description
} }),
}); HealthCheckDuration = report.TotalDuration
};
await context.Response.WriteAsJsonAsync(response);
}
});
}); });
} }
private class HealthCheckResponse private class HealthCheckResponse
{ {
public string Status { get; set; } public string Status { get; set; }
public IEnumerable<IndividualHealthCheckResponse> HealthChecks { get; set; }
public TimeSpan HealthCheckDuration { get; set; }
}
private class IndividualHealthCheckResponse public IEnumerable<IndividualHealthCheckResponse> HealthChecks { get; set; }
{
public string Status { get; set; } public TimeSpan HealthCheckDuration { get; set; }
public string Component { get; set; } }
public string Description { get; set; }
} private class IndividualHealthCheckResponse
{
public string Status { get; set; }
public string Component { get; set; }
public string Description { get; set; }
} }
} }

View File

@ -7,13 +7,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Todo.Infrastructure\Todo.Infrastructure.csproj" /> <ProjectReference Include="..\Todo.Infrastructure\Todo.Infrastructure.csproj"/>
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj" /> <ProjectReference Include="..\Todo.Core\Todo.Core.csproj"/>
<ProjectReference Include="..\Todo.Persistence\Todo.Persistence.csproj" /> <ProjectReference Include="..\Todo.Persistence\Todo.Persistence.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -6,30 +6,44 @@ using Todo.Core.Interfaces.User;
namespace Todo.Core.Application.Commands.Todo; namespace Todo.Core.Application.Commands.Todo;
public record CreateTodoCommand(string TodoTitle, string? TodoProject) : IRequest<string> public record CreateTodoCommand(
string TodoTitle,
string? TodoProject,
string? TodoDescription) : IRequest<string>
{ {
internal class Handler : IRequestHandler<CreateTodoCommand, string> internal class Handler : IRequestHandler<CreateTodoCommand, string>
{ {
private readonly ICurrentUserService _currentUserService; private readonly ICurrentUserService _currentUserService;
private readonly ITodoRepository _todoRepository;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly ITodoRepository _todoRepository;
public Handler(ICurrentUserService currentUserService, ITodoRepository todoRepository, IMediator mediator) public Handler(
ICurrentUserService currentUserService,
ITodoRepository todoRepository,
IMediator mediator)
{ {
_currentUserService = currentUserService; _currentUserService = currentUserService;
_todoRepository = todoRepository; _todoRepository = todoRepository;
_mediator = mediator; _mediator = mediator;
} }
public async Task<string> Handle(CreateTodoCommand request, CancellationToken cancellationToken) public async Task<string> Handle(
CreateTodoCommand request,
CancellationToken cancellationToken)
{ {
var userId = _currentUserService.GetUserId(); var userId = _currentUserService.GetUserId();
if (userId is null) if (userId is null)
throw new InvalidOperationException("User was not found"); throw new InvalidOperationException("User was not found");
var todo = await _todoRepository.CreateTodoAsync(request.TodoTitle, request.TodoProject, userId); var todo = await _todoRepository.CreateTodoAsync(
request.TodoTitle,
request.TodoProject,
request.TodoDescription,
userId);
await _mediator.Publish(new TodoCreated(todo.Id), cancellationToken); await _mediator.Publish(
new TodoCreated(todo.Id),
cancellationToken);
return todo.Id; return todo.Id;
} }

View File

@ -2,14 +2,12 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using MediatR; using MediatR;
using Microsoft.AspNetCore.SignalR;
using Todo.Core.Application.Services.UserConnectionStore;
using Todo.Core.Interfaces.Publisher; using Todo.Core.Interfaces.Publisher;
using Todo.Core.Interfaces.User;
namespace Todo.Core.Application.Notifications.Todo; namespace Todo.Core.Application.Notifications.Todo;
public record TodoCreated([property: JsonPropertyName("todoId")] string TodoId) : INotification public record TodoCreated(
[property: JsonPropertyName("todoId")] string TodoId) : INotification
{ {
internal class Handler : INotificationHandler<TodoCreated> internal class Handler : INotificationHandler<TodoCreated>
{ {
@ -20,9 +18,13 @@ public record TodoCreated([property: JsonPropertyName("todoId")] string TodoId)
_todoPublisher = todoPublisher; _todoPublisher = todoPublisher;
} }
public async Task Handle(TodoCreated notification, CancellationToken cancellationToken) public async Task Handle(
TodoCreated notification,
CancellationToken cancellationToken)
{ {
await _todoPublisher.Publish(JsonSerializer.Serialize(notification), cancellationToken); await _todoPublisher.Publish(
JsonSerializer.Serialize(notification),
cancellationToken);
} }
} }
} }

View File

@ -4,5 +4,5 @@ namespace Todo.Core.Interfaces.Publisher;
public interface ITodoPublisher public interface ITodoPublisher
{ {
Task Publish(string todoId, CancellationToken cancellationToken = new ()); Task Publish(string todoId, CancellationToken cancellationToken = new());
} }

View File

@ -5,4 +5,4 @@ public interface IUserConnectionStore
Task AddAsync(string userId, string connectionId); Task AddAsync(string userId, string connectionId);
Task RemoveAsync(string userId, string connectionId); Task RemoveAsync(string userId, string connectionId);
Task<IEnumerable<string>> GetConnectionsAsync(string userId); Task<IEnumerable<string>> GetConnectionsAsync(string userId);
} }

View File

@ -10,5 +10,8 @@ namespace Todo.Core;
public static class DependencyInjection public static class DependencyInjection
{ {
public static IServiceCollection AddCore(this IServiceCollection services) => services.AddMediatR(Assembly.GetExecutingAssembly()); public static IServiceCollection AddCore(this IServiceCollection services)
{
return services.AddMediatR(Assembly.GetExecutingAssembly());
}
} }

View File

@ -5,6 +5,7 @@ public record Todo
public string Id { get; init; } public string Id { get; init; }
public string Title { get; init; } public string Title { get; init; }
public bool Status { get; init; } public bool Status { get; init; }
public string Project { get; init; } public string? Project { get; init; }
public string AuthorId { get; init; } public string AuthorId { get; init; }
public string? Description { get; init; }
} }

View File

@ -1,8 +1,7 @@
namespace Todo.Core.Entities namespace Todo.Core.Entities;
public class User
{ {
public class User public string Id { get; set; }
{ public string Email { get; set; }
public string Id { get; set; }
public string Email { get; set; }
}
} }

View File

@ -1,11 +1,20 @@
namespace Todo.Core.Interfaces.Persistence; namespace Todo.Core.Interfaces.Persistence;
public interface ITodoRepository public interface ITodoRepository
{ {
Task<Entities.Todo> CreateTodoAsync(string title, string projectName, string userId); Task<Entities.Todo> CreateTodoAsync(
string title,
string? projectName,
string? description,
string userId);
Task<IEnumerable<Entities.Todo>> GetTodosAsync(); Task<IEnumerable<Entities.Todo>> GetTodosAsync();
Task UpdateTodoStatus(string todoId, bool todoStatus, string userId);
Task UpdateTodoStatus(
string todoId,
bool todoStatus,
string userId);
Task<IEnumerable<Entities.Todo>> GetNotDoneTodos(); Task<IEnumerable<Entities.Todo>> GetNotDoneTodos();
Task<Entities.Todo> GetTodoByIdAsync(string todoId); Task<Entities.Todo> GetTodoByIdAsync(string todoId);
Task<Entities.Todo> UpdateTodoAsync(Entities.Todo todo); Task<Entities.Todo> UpdateTodoAsync(Entities.Todo todo);

View File

@ -1,5 +1,3 @@
using Todo.Core.Entities;
namespace Todo.Core.Interfaces.Persistence; namespace Todo.Core.Interfaces.Persistence;
public interface IUserRepository public interface IUserRepository

View File

@ -4,18 +4,18 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Application\Services" /> <Folder Include="Application\Services"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Mediatr" Version="9.0.0" /> <PackageReference Include="Mediatr" Version="9.0.0"/>
<PackageReference Include="Mediatr.Extensions.Microsoft.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Mediatr.Extensions.Microsoft.DependencyInjection" Version="9.0.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -10,7 +10,9 @@ namespace Todo.Infrastructure;
public static class DependencyInjection public static class DependencyInjection
{ {
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{ {
var giteaAuthOptions = new GiteaAuthOptions(); var giteaAuthOptions = new GiteaAuthOptions();
var giteaOptions = configuration.GetRequiredSection("GITEA"); var giteaOptions = configuration.GetRequiredSection("GITEA");
@ -21,35 +23,49 @@ public static class DependencyInjection
.Bind(giteaOptions) .Bind(giteaOptions)
.ValidateDataAnnotations(); .ValidateDataAnnotations();
services.AddSingleton<IUserConnectionStore, InMemoryUserConnectionStore>(); services
.AddSingleton<IUserConnectionStore, InMemoryUserConnectionStore>();
return services.AddAuthentication(options => return services.AddAuthentication(
{ options =>
options.DefaultScheme = "Cookies"; {
options.DefaultChallengeScheme = "oidc"; options.DefaultScheme = "Cookies";
}) options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies") .AddCookie("Cookies")
.AddOpenIdConnect("oidc", options => .AddOpenIdConnect(
{ "oidc",
options.Authority = giteaAuthOptions.Url; options =>
options.ClientId = giteaAuthOptions.ClientId; {
options.ClientSecret = giteaAuthOptions.ClientSecret; options.Authority = giteaAuthOptions.Url;
options.ResponseType = "code"; options.ClientId = giteaAuthOptions.ClientId;
options.ClientSecret = giteaAuthOptions.ClientSecret;
options.ResponseType = "code";
options.SaveTokens = true; options.SaveTokens = true;
}).Services; })
.Services;
} }
public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) => app.UseCookiePolicy( public static IApplicationBuilder UseInfrastructure(
new CookiePolicyOptions this IApplicationBuilder app)
{ {
Secure = CookieSecurePolicy.Always return app.UseCookiePolicy(
}); new CookiePolicyOptions
{
Secure = CookieSecurePolicy.Always
});
}
} }
public class GiteaAuthOptions public class GiteaAuthOptions
{ {
[Required] public string Url { get; set; } [Required]
[Required] public string ClientId { get; init; } public string Url { get; set; }
[Required] public string ClientSecret { get; init; }
[Required]
public string ClientId { get; init; }
[Required]
public string ClientSecret { get; init; }
} }

View File

@ -5,17 +5,17 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj" /> <ProjectReference Include="..\Todo.Core\Todo.Core.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -3,9 +3,10 @@ using Todo.Core.Application.Services.UserConnectionStore;
namespace Todo.Infrastructure.UserConnectionStore; namespace Todo.Infrastructure.UserConnectionStore;
class InMemoryUserConnectionStore : IUserConnectionStore internal class InMemoryUserConnectionStore : IUserConnectionStore
{ {
private static readonly ConcurrentDictionary<string, List<string>> ConnectedUsers = new(); private static readonly ConcurrentDictionary<string, List<string>>
ConnectedUsers = new();
public Task AddAsync(string userId, string connectionId) public Task AddAsync(string userId, string connectionId)
{ {
@ -27,7 +28,10 @@ class InMemoryUserConnectionStore : IUserConnectionStore
public Task<IEnumerable<string>> GetConnectionsAsync(string userId) public Task<IEnumerable<string>> GetConnectionsAsync(string userId)
{ {
ConnectedUsers.TryGetValue(userId, out var connections); ConnectedUsers.TryGetValue(userId, out var connections);
return Task.FromResult(connections is null ? new List<string>().AsEnumerable() : connections.AsEnumerable()); return Task.FromResult(
connections is null
? new List<string>().AsEnumerable()
: connections.AsEnumerable());
} }
public Task RemoveAsync(string userId, string connectionId) public Task RemoveAsync(string userId, string connectionId)
@ -39,12 +43,10 @@ class InMemoryUserConnectionStore : IUserConnectionStore
// If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers). // If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers).
if (existingUserConnectionIds?.Count == 0) if (existingUserConnectionIds?.Count == 0)
{
// if there are no connections for the user, // if there are no connections for the user,
// just delete the userName key from the ConnectedUsers concurent dictionary // just delete the userName key from the ConnectedUsers concurent dictionary
ConnectedUsers.TryRemove(userId, out _); ConnectedUsers.TryRemove(userId, out _);
}
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@ -4,32 +4,32 @@ global using System.Linq;
global using System.Collections.Generic; global using System.Collections.Generic;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Todo.Core.Interfaces.Persistence; using Todo.Core.Interfaces.Persistence;
using Todo.Persistence.Mongo; using Todo.Persistence.Mongo;
using Todo.Persistence.Mongo.Repositories; using Todo.Persistence.Mongo.Repositories;
namespace Todo.Persistence namespace Todo.Persistence;
public static class DependencyInjection
{ {
public static class DependencyInjection public static IServiceCollection AddPersistence(
this IServiceCollection services,
IConfiguration configuration,
out MongoDbOptions mongoDbOptions)
{ {
public static IServiceCollection AddPersistence(this IServiceCollection services, IConfiguration configuration, var exportableMongoDbOptions = new MongoDbOptions();
out MongoDbOptions mongoDbOptions) var options = configuration.GetRequiredSection("MONGODB");
{ options.Bind(exportableMongoDbOptions);
var exportableMongoDbOptions = new MongoDbOptions(); mongoDbOptions = exportableMongoDbOptions;
var options = configuration.GetRequiredSection("MONGODB");
options.Bind(exportableMongoDbOptions);
mongoDbOptions = exportableMongoDbOptions;
services services
.AddOptions<MongoDbOptions>() .AddOptions<MongoDbOptions>()
.Bind(options) .Bind(options)
.ValidateDataAnnotations(); .ValidateDataAnnotations();
return services return services
.AddSingleton<MongoDbConnectionHandler>() .AddSingleton<MongoDbConnectionHandler>()
.AddScoped<IUserRepository, UserRepository>() .AddScoped<IUserRepository, UserRepository>()
.AddScoped<ITodoRepository, TodoRepository>(); .AddScoped<ITodoRepository, TodoRepository>();
}
} }
} }

View File

@ -3,33 +3,42 @@ using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver; using MongoDB.Driver;
using Todo.Persistence.Mongo.Repositories.Dtos; using Todo.Persistence.Mongo.Repositories.Dtos;
namespace Todo.Persistence.Mongo namespace Todo.Persistence.Mongo;
public static class Migrations
{ {
public static class Migrations public static IApplicationBuilder MigrateMongoDb(
this IApplicationBuilder app)
{ {
public static IApplicationBuilder MigrateMongoDb(this IApplicationBuilder app) Task.Run(
{ async () =>
Task.Run(async () => {
{ var connectionHandler = app.ApplicationServices
var connectionHandler = app.ApplicationServices.GetRequiredService<MongoDbConnectionHandler>(); .GetRequiredService<MongoDbConnectionHandler>();
var database = connectionHandler.CreateDatabaseConnection(); var database = connectionHandler.CreateDatabaseConnection();
await CreateIndexes(database); await CreateIndexes(database);
}).Wait(10000); })
.Wait(10000);
return app; return app;
} }
private static async Task CreateIndexes(IMongoDatabase mongoDatabase) private static async Task CreateIndexes(IMongoDatabase mongoDatabase)
{ {
Console.WriteLine("Running: CreateIndexes"); Console.WriteLine("Running: CreateIndexes");
var collection = mongoDatabase.GetCollection<MongoUser>("users"); var collection = mongoDatabase.GetCollection<MongoUser>("users");
var indexKeysDefinition = Builders<MongoUser>.IndexKeys.Ascending(user => user.Email); var indexKeysDefinition =
var indexModel = Builders<MongoUser>.IndexKeys.Ascending(user => user.Email);
new CreateIndexModel<MongoUser>(indexKeysDefinition, new CreateIndexOptions() { Unique = true }); var indexModel =
new CreateIndexModel<MongoUser>(
indexKeysDefinition,
new CreateIndexOptions
{
Unique = true
});
await collection.Indexes.CreateOneAsync(indexModel); await collection.Indexes.CreateOneAsync(indexModel);
Console.WriteLine("Finished: CreateIndexes"); Console.WriteLine("Finished: CreateIndexes");
}
} }
} }

View File

@ -7,7 +7,8 @@ public class MongoDbConnectionHandler
{ {
private readonly IOptionsMonitor<MongoDbOptions> _optionsMonitor; private readonly IOptionsMonitor<MongoDbOptions> _optionsMonitor;
public MongoDbConnectionHandler(IOptionsMonitor<MongoDbOptions> optionsMonitor) public MongoDbConnectionHandler(
IOptionsMonitor<MongoDbOptions> optionsMonitor)
{ {
_optionsMonitor = optionsMonitor; _optionsMonitor = optionsMonitor;
} }
@ -19,7 +20,8 @@ public class MongoDbConnectionHandler
return CreateConnectionFromOptions(options); return CreateConnectionFromOptions(options);
} }
private static IMongoDatabase CreateConnectionFromOptions(MongoDbOptions options) private static IMongoDatabase CreateConnectionFromOptions(
MongoDbOptions options)
{ {
var conn = new MongoClient(FormatConnectionString(options)); var conn = new MongoClient(FormatConnectionString(options));
var database = conn.GetDatabase(options.Database); var database = conn.GetDatabase(options.Database);
@ -27,6 +29,9 @@ public class MongoDbConnectionHandler
return database; return database;
} }
public static string FormatConnectionString(MongoDbOptions options) => public static string FormatConnectionString(MongoDbOptions options)
$"mongodb://{options.Username}:{options.Password}@{options.Host}:{options.Port}"; {
return
$"mongodb://{options.Username}:{options.Password}@{options.Host}:{options.Port}";
}
} }

View File

@ -6,9 +6,18 @@ public class MongoDbOptions
{ {
public const string MongoDb = "MongoDb"; public const string MongoDb = "MongoDb";
[Required] public string Username { get; set; } [Required]
[Required] public string Password { get; set; } public string Username { get; set; }
[Required] public string Host { get; set; }
[Required] public string Port { get; set; } [Required]
[Required] public string Database { get; set; } public string Password { get; set; }
[Required]
public string Host { get; set; }
[Required]
public string Port { get; set; }
[Required]
public string Database { get; set; }
} }

View File

@ -9,8 +9,14 @@ public record MongoTodo
[BsonRepresentation(BsonType.ObjectId)] [BsonRepresentation(BsonType.ObjectId)]
public string Id { get; init; } public string Id { get; init; }
[BsonRequired] public string Title { get; init; } [BsonRequired]
[BsonRequired] public bool Status { get; set; } public string Title { get; init; }
public string ProjectName { get; set; } = String.Empty;
public string? Description { get; init; }
[BsonRequired]
public bool Status { get; set; }
public string? ProjectName { get; set; } = string.Empty;
public string AuthorId { get; set; } public string AuthorId { get; set; }
} }

View File

@ -1,15 +1,17 @@
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
namespace Todo.Persistence.Mongo.Repositories.Dtos namespace Todo.Persistence.Mongo.Repositories.Dtos;
{
public record MongoUser
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; init; }
[BsonRequired] public string Email { get; init; } public record MongoUser
[BsonRequired] public string Password { get; set; } {
} [BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; init; }
[BsonRequired]
public string Email { get; init; }
[BsonRequired]
public string Password { get; set; }
} }

View File

@ -15,12 +15,28 @@ public class TodoRepository : ITodoRepository
_todosCollection = database.GetCollection<MongoTodo>("todos"); _todosCollection = database.GetCollection<MongoTodo>("todos");
} }
public async Task<Core.Entities.Todo> CreateTodoAsync(string title, string projectName, string userId) public async Task<Core.Entities.Todo> CreateTodoAsync(
string title,
string projectName,
string? description,
string userId)
{ {
var todo = new MongoTodo() { Title = title, ProjectName = projectName, AuthorId = userId }; var todo = new MongoTodo
{
Title = title,
ProjectName = projectName,
AuthorId = userId,
Description = description
};
await _todosCollection.InsertOneAsync(todo); await _todosCollection.InsertOneAsync(todo);
return new Core.Entities.Todo() return new Core.Entities.Todo
{ Id = todo.Id, Title = todo.Title, Status = false, Project = todo.ProjectName }; {
Id = todo.Id,
Title = todo.Title,
Status = false,
Project = todo.ProjectName,
Description = todo.Description
};
} }
public async Task<IEnumerable<Core.Entities.Todo>> GetTodosAsync() public async Task<IEnumerable<Core.Entities.Todo>> GetTodosAsync()
@ -28,14 +44,26 @@ public class TodoRepository : ITodoRepository
var todos = await _todosCollection.FindAsync(_ => true); var todos = await _todosCollection.FindAsync(_ => true);
return todos return todos
.ToEnumerable() .ToEnumerable()
.Select(t => .Select(
new Core.Entities.Todo() { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.ProjectName }); t =>
new Core.Entities.Todo
{
Id = t.Id,
Title = t.Title,
Status = t.Status,
Project = t.ProjectName,
Description = t.Description
});
} }
public async Task UpdateTodoStatus(string todoId, bool todoStatus, string userId) public async Task UpdateTodoStatus(
string todoId,
bool todoStatus,
string userId)
{ {
await _todosCollection await _todosCollection
.UpdateOneAsync(t => t.Id == todoId && t.AuthorId == userId, .UpdateOneAsync(
t => t.Id == todoId && t.AuthorId == userId,
Builders<MongoTodo>.Update.Set(t => t.Status, todoStatus)); Builders<MongoTodo>.Update.Set(t => t.Status, todoStatus));
} }
@ -50,33 +78,39 @@ public class TodoRepository : ITodoRepository
var todoCursor = await _todosCollection.FindAsync(f => f.Id == todoId); var todoCursor = await _todosCollection.FindAsync(f => f.Id == todoId);
var todo = await todoCursor.FirstOrDefaultAsync(); var todo = await todoCursor.FirstOrDefaultAsync();
return new Core.Entities.Todo() return new Core.Entities.Todo
{ {
Id = todo.Id, Id = todo.Id,
Project = todo.ProjectName, Project = todo.ProjectName,
Status = todo.Status, Status = todo.Status,
Title = todo.Title Title = todo.Title,
Description = todo.Description
}; };
} }
public async Task<Core.Entities.Todo> UpdateTodoAsync(Core.Entities.Todo todo) public async Task<Core.Entities.Todo> UpdateTodoAsync(
Core.Entities.Todo todo)
{ {
var updatedTodo = await _todosCollection.FindOneAndReplaceAsync<MongoTodo>(f => f.Id == todo.Id, new MongoTodo() var updatedTodo = await _todosCollection.FindOneAndReplaceAsync(
{ f => f.Id == todo.Id,
Id = todo.Id, new MongoTodo
Status = todo.Status, {
Title = todo.Title, Id = todo.Id,
ProjectName = todo.Project, Status = todo.Status,
AuthorId = todo.AuthorId Title = todo.Title,
}); ProjectName = todo.Project,
AuthorId = todo.AuthorId,
Description = todo.Description
});
return new Core.Entities.Todo() return new Core.Entities.Todo
{ {
Id = updatedTodo.Id, Id = updatedTodo.Id,
Project = updatedTodo.ProjectName, Project = updatedTodo.ProjectName,
Status = updatedTodo.Status, Status = updatedTodo.Status,
Title = updatedTodo.Title, Title = updatedTodo.Title,
AuthorId = updatedTodo.AuthorId AuthorId = updatedTodo.AuthorId,
Description = updatedTodo.Description
}; };
} }
} }

View File

@ -3,23 +3,30 @@ using Todo.Core.Entities;
using Todo.Core.Interfaces.Persistence; using Todo.Core.Interfaces.Persistence;
using Todo.Persistence.Mongo.Repositories.Dtos; using Todo.Persistence.Mongo.Repositories.Dtos;
namespace Todo.Persistence.Mongo.Repositories namespace Todo.Persistence.Mongo.Repositories;
public class UserRepository : IUserRepository
{ {
public class UserRepository : IUserRepository private readonly IMongoCollection<MongoUser> _usersCollection;
public UserRepository(MongoDbConnectionHandler mongoDbConnectionHandler)
{ {
private readonly IMongoCollection<MongoUser> _usersCollection; var database = mongoDbConnectionHandler.CreateDatabaseConnection();
_usersCollection = database.GetCollection<MongoUser>("users");
}
public UserRepository(MongoDbConnectionHandler mongoDbConnectionHandler) public async Task<User> Register(string email, string password)
{
var dtoUser = new MongoUser
{ {
var database = mongoDbConnectionHandler.CreateDatabaseConnection(); Email = email,
_usersCollection = database.GetCollection<MongoUser>("users"); Password = password
} };
await _usersCollection.InsertOneAsync(dtoUser);
public async Task<User> Register(string email, string password) return new User
{ {
var dtoUser = new MongoUser() { Email = email, Password = password }; Email = dtoUser.Email,
await _usersCollection.InsertOneAsync(dtoUser); Id = dtoUser.Id
return new User() { Email = dtoUser.Email, Id = dtoUser.Id }; };
}
} }
} }

View File

@ -2,20 +2,21 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.13.2" /> <PackageReference Include="MongoDB.Driver" Version="2.13.2"/>
<PackageReference Include="MongoDB.Driver.Core" Version="2.13.2" /> <PackageReference Include="MongoDB.Driver.Core" Version="2.13.2"/>
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="5.0.1" /> <PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="5.0.1"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj" /> <ProjectReference Include="..\Todo.Core\Todo.Core.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -4,7 +4,7 @@ import { PrimaryButton } from "@src/components/common/buttons/primaryButton";
import { TodoShortForm } from "@src/components/todos/collapsed/todoShortForm"; import { TodoShortForm } from "@src/components/todos/collapsed/todoShortForm";
export const AddTodoForm: FC<{ export const AddTodoForm: FC<{
onAdd: (todoName: string, project: string) => void; onAdd: (todoName: string, project: string, description: string) => void;
onClose: () => void; onClose: () => void;
project: string; project: string;
}> = ({ onAdd, onClose, ...props }) => { }> = ({ onAdd, onClose, ...props }) => {
@ -16,7 +16,7 @@ export const AddTodoForm: FC<{
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
onAdd(todoName, project); onAdd(todoName, project, todoDescription);
setTodoName(""); setTodoName("");
setTodoDescription(""); setTodoDescription("");
}} }}

View File

@ -22,8 +22,8 @@ export function AddTodo(props: { project: string }) {
return ( return (
<AddTodoForm <AddTodoForm
project={props.project} project={props.project}
onAdd={(todoName, project) => { onAdd={(todoName, project, description) => {
createTodo(todoName, project); createTodo(todoName, project, description);
}} }}
onClose={() => setCollapsed(CollapsedState.collapsed)} onClose={() => setCollapsed(CollapsedState.collapsed)}
/> />

View File

@ -29,7 +29,12 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
{props.todo.title} {props.todo.title}
</a> </a>
</Link> </Link>
<div> <div className="flex flex-row justify-between items-end">
<div>
{props.todo.description && (
<div className="h-3 w-3 bg-gray-200 dark:bg-gray-900"/>
)}
</div>
{props.displayProject && props.todo.project && ( {props.displayProject && props.todo.project && (
<div className="text-gray-500 text-xs text-right whitespace-nowrap place-self-end"> <div className="text-gray-500 text-xs text-right whitespace-nowrap place-self-end">
{props.todo.project} {props.todo.project}

View File

@ -9,6 +9,7 @@ export const StatusState: { done: Done; notDone: NotDone } = {
export interface Todo { export interface Todo {
id: string; id: string;
title: string; title: string;
description?: string;
status: StatusState; status: StatusState;
project?: string; project?: string;
} }

View File

@ -12,7 +12,7 @@ interface SocketContextProps {
inboxTodos: Todo[]; inboxTodos: Todo[];
getTodos: () => void; getTodos: () => void;
getInboxTodos: () => void; getInboxTodos: () => void;
createTodo: (todoName: string, project: string) => void; createTodo: (todoName: string, project: string, description: string) => void;
updateTodo: (todoId: string, todoStatus: StatusState) => void; updateTodo: (todoId: string, todoStatus: StatusState) => void;
getTodoById(todoId: string): void; getTodoById(todoId: string): void;
replaceTodo(todo: Todo): void; replaceTodo(todo: Todo): void;
@ -24,7 +24,7 @@ export const SocketContext = createContext<SocketContextProps>({
inboxTodos: [], inboxTodos: [],
getTodos: () => {}, getTodos: () => {},
getInboxTodos: () => {}, getInboxTodos: () => {},
createTodo: (todoName, project) => {}, createTodo: (todoName, project, description) => {},
updateTodo: (todoId, todoStatus) => {}, updateTodo: (todoId, todoStatus) => {},
getTodoById(todoId: string) {}, getTodoById(todoId: string) {},
replaceTodo(todo: Todo) {}, replaceTodo(todo: Todo) {},
@ -89,8 +89,8 @@ export const SocketProvider: FC = (props) => {
getInboxTodos: () => { getInboxTodos: () => {
conn.invoke("GetInboxTodos").catch(console.error); conn.invoke("GetInboxTodos").catch(console.error);
}, },
createTodo: (todoName, project) => { createTodo: (todoName, project, description) => {
conn.invoke("CreateTodo", todoName, project).catch(console.error); conn.invoke("CreateTodo", todoName, project, description).catch(console.error);
}, },
updateTodo: (todoId, todoStatus) => { updateTodo: (todoId, todoStatus) => {
conn.invoke("UpdateTodo", todoId, todoStatus).catch(console.error); conn.invoke("UpdateTodo", todoId, todoStatus).catch(console.error);

View File

@ -19,8 +19,8 @@ export const useCreateTodo = () => {
const socketContext = useContext(SocketContext); const socketContext = useContext(SocketContext);
return { return {
createTodo: (todoName: string, project: string) => { createTodo: (todoName: string, project: string, description: string) => {
socketContext.createTodo(todoName, project); socketContext.createTodo(todoName, project, description);
}, },
}; };
}; };