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,18 +1,17 @@
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]
[Route("api/auth")]
[Authorize]
[AllowAnonymous]
public class AuthController : ControllerBase
{
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
public AuthController(IUserRepository userRepository) public AuthController(IUserRepository userRepository)
@ -21,9 +20,12 @@ namespace Todo.Api.Controllers
} }
[HttpPost("register")] [HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterUserRequest request) public async Task<IActionResult> Register(
[FromBody] RegisterUserRequest request)
{ {
var user = await _userRepository.Register(request.Email, request.Password); var user = await _userRepository.Register(
request.Email,
request.Password);
return Ok(user); return Ok(user);
} }
@ -36,7 +38,9 @@ namespace Todo.Api.Controllers
RedirectUri = Url.Action(nameof(Callback)), RedirectUri = Url.Action(nameof(Callback)),
Items = Items =
{ {
{"returnUrl", returnUrl} {
"returnUrl", returnUrl
}
} }
}; };
return Challenge(props); return Challenge(props);
@ -49,9 +53,7 @@ namespace Todo.Api.Controllers
var result = var result =
await HttpContext.AuthenticateAsync("oidc"); await HttpContext.AuthenticateAsync("oidc");
if (result?.Succeeded != true) if (result?.Succeeded != true)
{
throw new Exception("External authentication error"); throw new Exception("External authentication error");
}
var returnUrl = result.Properties?.Items["returnUrl"] ?? "~/"; var returnUrl = result.Properties?.Items["returnUrl"] ?? "~/";
return Redirect(returnUrl); return Redirect(returnUrl);
@ -59,8 +61,10 @@ namespace Todo.Api.Controllers
public record RegisterUserRequest public record RegisterUserRequest
{ {
[Required] public string Email { get; init; } [Required]
[Required] public string Password { get; init; } 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

@ -10,9 +10,12 @@ public record TodoResponse
[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

@ -14,5 +14,8 @@ public record UpdateTodoRequest
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
.Select(
t => new TodoResponse
{
Id = t.Id,
Title = t.Title,
Status = t.Status,
Project = t.Project,
Description = t.Description
})
.ToList()); .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
.Select(
t => new TodoResponse
{
Id = t.Id,
Title = t.Title,
Status = t.Status,
Project = t.Project,
Description = t.Description
})
.ToList()); .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, Id = todo.Id,
Project = todo.Project, Project = todo.Project,
Status = todo.Status, Status = todo.Status,
Title = todo.Title, 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, Id = updateTodo.Id,
Project = updateTodo.Project, Project = updateTodo.Project,
Status = updateTodo.Status, Status = updateTodo.Status,
Title = updateTodo.Title, Title = updateTodo.Title,
AuthorId = userId AuthorId = userId,
Description = updateTodo.Description
}); });
var serializedTodo = JsonSerializer.Serialize(new TodoResponse() var serializedTodo = JsonSerializer.Serialize(
new TodoResponse
{ {
Id = updatedTodo.Id, Id = updatedTodo.Id,
Project = updatedTodo.Project, Project = updatedTodo.Project,
Status = updatedTodo.Status, Status = updatedTodo.Status,
Title = updatedTodo.Title, 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() ?? {
return _currentUserService.GetUserId() ??
throw new InvalidOperationException("User id was invalid. Something has gone terribly wrong"); 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) {
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
} }
} }

View File

@ -10,10 +10,10 @@ 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,
@ -27,15 +27,22 @@ public class TodoPublisher : ITodoPublisher
_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 _userConnectionStore.GetConnectionsAsync(userId);
await _hubContext await _hubContext
.Clients .Clients
.Clients(connections) .Clients(connections)
.SendAsync("todoCreated", todoId , cancellationToken); .SendAsync(
"todoCreated",
todoId,
cancellationToken);
_logger.LogInformation("todo created {TodoId}", todoId); _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,17 +10,17 @@ 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;
@ -33,22 +33,34 @@ namespace Todo.Api
{ {
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 =>
builder.WithOrigins(
"http://localhost:3000",
"https://todo.front.kjuulh.io")
.AllowCredentials() .AllowCredentials()
.AllowAnyHeader() .AllowAnyHeader()
.AllowAnyMethod()); .AllowAnyMethod());
}); });
services.AddCore(); services.AddCore();
services.AddScoped<ICurrentUserService, HttpContextCurrentUserService>(); services
.AddScoped<ICurrentUserService, HttpContextCurrentUserService>();
services.AddScoped<ITodoPublisher, TodoPublisher>(); 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;
@ -57,7 +69,9 @@ namespace Todo.Api
services.AddPersistence(Configuration, out var mongoDbOptions); services.AddPersistence(Configuration, out var mongoDbOptions);
services services
.AddHealthChecks() .AddHealthChecks()
.AddMongoDb(MongoDbConnectionHandler.FormatConnectionString(mongoDbOptions)); .AddMongoDb(
MongoDbConnectionHandler
.FormatConnectionString(mongoDbOptions));
services.AddSignalR(); services.AddSignalR();
} }
@ -70,7 +84,10 @@ namespace Todo.Api
{ {
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Todo.Api v1")); app.UseSwaggerUI(
c => c.SwaggerEndpoint(
"/swagger/v1/swagger.json",
"Todo.Api v1"));
} }
app.UseRouting(); app.UseRouting();
@ -79,18 +96,22 @@ namespace Todo.Api
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseEndpoints(endpoints => 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",
new HealthCheckOptions
{ {
ResponseWriter = async (context, report) => ResponseWriter = async (context, report) =>
{ {
var response = new HealthCheckResponse() var response = new HealthCheckResponse
{ {
Status = report.Status.ToString(), Status = report.Status.ToString(),
HealthChecks = report.Entries.Select(x => new IndividualHealthCheckResponse HealthChecks = report.Entries.Select(
x => new IndividualHealthCheckResponse
{ {
Component = x.Key, Component = x.Key,
Status = x.Value.Status.ToString(), Status = x.Value.Status.ToString(),
@ -107,7 +128,9 @@ namespace Todo.Api
private class HealthCheckResponse private class HealthCheckResponse
{ {
public string Status { get; set; } public string Status { get; set; }
public IEnumerable<IndividualHealthCheckResponse> HealthChecks { get; set; } public IEnumerable<IndividualHealthCheckResponse> HealthChecks { get; set; }
public TimeSpan HealthCheckDuration { get; set; } public TimeSpan HealthCheckDuration { get; set; }
} }
@ -117,5 +140,4 @@ namespace Todo.Api
public string Component { get; set; } public string Component { get; set; }
public string Description { 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

@ -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 Id { get; set; }
public string Email { 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

@ -6,16 +6,16 @@
</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,15 +23,19 @@ 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.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc"; options.DefaultChallengeScheme = "oidc";
}) })
.AddCookie("Cookies") .AddCookie("Cookies")
.AddOpenIdConnect("oidc", options => .AddOpenIdConnect(
"oidc",
options =>
{ {
options.Authority = giteaAuthOptions.Url; options.Authority = giteaAuthOptions.Url;
options.ClientId = giteaAuthOptions.ClientId; options.ClientId = giteaAuthOptions.ClientId;
@ -37,19 +43,29 @@ public static class DependencyInjection
options.ResponseType = "code"; options.ResponseType = "code";
options.SaveTokens = true; options.SaveTokens = true;
}).Services; })
.Services;
} }
public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) => app.UseCookiePolicy( public static IApplicationBuilder UseInfrastructure(
this IApplicationBuilder app)
{
return app.UseCookiePolicy(
new CookiePolicyOptions new CookiePolicyOptions
{ {
Secure = CookieSecurePolicy.Always 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

@ -7,15 +7,15 @@
</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,11 +43,9 @@ 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,16 +4,17 @@ 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,
public static IServiceCollection AddPersistence(this IServiceCollection services, IConfiguration configuration, IConfiguration configuration,
out MongoDbOptions mongoDbOptions) out MongoDbOptions mongoDbOptions)
{ {
var exportableMongoDbOptions = new MongoDbOptions(); var exportableMongoDbOptions = new MongoDbOptions();
@ -31,5 +32,4 @@ namespace Todo.Persistence
.AddScoped<IUserRepository, UserRepository>() .AddScoped<IUserRepository, UserRepository>()
.AddScoped<ITodoRepository, TodoRepository>(); .AddScoped<ITodoRepository, TodoRepository>();
} }
}
} }

View File

@ -3,19 +3,23 @@ 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
{ .GetRequiredService<MongoDbConnectionHandler>();
var connectionHandler = app.ApplicationServices.GetRequiredService<MongoDbConnectionHandler>();
var database = connectionHandler.CreateDatabaseConnection(); var database = connectionHandler.CreateDatabaseConnection();
await CreateIndexes(database); await CreateIndexes(database);
}).Wait(10000); })
.Wait(10000);
return app; return app;
} }
@ -24,12 +28,17 @@ namespace Todo.Persistence.Mongo
{ {
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 =
Builders<MongoUser>.IndexKeys.Ascending(user => user.Email);
var indexModel = var indexModel =
new CreateIndexModel<MongoUser>(indexKeysDefinition, new CreateIndexOptions() { Unique = true }); 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)
{
return
$"mongodb://{options.Username}:{options.Password}@{options.Host}:{options.Port}"; $"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
{ {
public record MongoUser
{
[BsonId] [BsonId]
[BsonRepresentation(BsonType.ObjectId)] [BsonRepresentation(BsonType.ObjectId)]
public string Id { get; init; } public string Id { get; init; }
[BsonRequired] public string Email { get; init; } [BsonRequired]
[BsonRequired] public string Password { get; set; } 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,
new MongoTodo
{ {
Id = todo.Id, Id = todo.Id,
Status = todo.Status, Status = todo.Status,
Title = todo.Title, Title = todo.Title,
ProjectName = todo.Project, ProjectName = todo.Project,
AuthorId = todo.AuthorId 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,10 +3,10 @@ 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; private readonly IMongoCollection<MongoUser> _usersCollection;
public UserRepository(MongoDbConnectionHandler mongoDbConnectionHandler) public UserRepository(MongoDbConnectionHandler mongoDbConnectionHandler)
@ -17,9 +17,16 @@ namespace Todo.Persistence.Mongo.Repositories
public async Task<User> Register(string email, string password) public async Task<User> Register(string email, string password)
{ {
var dtoUser = new MongoUser() { Email = email, Password = password }; var dtoUser = new MongoUser
{
Email = email,
Password = password
};
await _usersCollection.InsertOneAsync(dtoUser); await _usersCollection.InsertOneAsync(dtoUser);
return new User() { Email = dtoUser.Email, 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 className="flex flex-row justify-between items-end">
<div> <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);
}, },
}; };
}; };