Add description
This commit is contained in:
parent
afb3bfd681
commit
c281d5a6a7
@ -1,12 +1,11 @@
|
|||||||
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]
|
[ApiController]
|
||||||
[Route("api/auth")]
|
[Route("api/auth")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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; }
|
||||||
}
|
}
|
@ -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; }
|
||||||
}
|
}
|
@ -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");
|
||||||
}
|
}
|
||||||
|
}
|
@ -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>(); });
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
@ -10,15 +10,15 @@ 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)
|
||||||
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,4 +141,3 @@ namespace Todo.Api
|
|||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
@ -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; }
|
||||||
}
|
}
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
|
@ -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);
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
using Todo.Core.Entities;
|
|
||||||
|
|
||||||
namespace Todo.Core.Interfaces.Persistence;
|
namespace Todo.Core.Interfaces.Persistence;
|
||||||
|
|
||||||
public interface IUserRepository
|
public interface IUserRepository
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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, IConfiguration configuration,
|
public static IServiceCollection AddPersistence(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfiguration configuration,
|
||||||
out MongoDbOptions mongoDbOptions)
|
out MongoDbOptions mongoDbOptions)
|
||||||
{
|
{
|
||||||
var exportableMongoDbOptions = new MongoDbOptions();
|
var exportableMongoDbOptions = new MongoDbOptions();
|
||||||
@ -32,4 +33,3 @@ namespace Todo.Persistence
|
|||||||
.AddScoped<ITodoRepository, TodoRepository>();
|
.AddScoped<ITodoRepository, TodoRepository>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
@ -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}";
|
||||||
}
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
}
|
}
|
@ -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; }
|
||||||
}
|
}
|
@ -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; }
|
||||||
}
|
}
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,8 +3,8 @@ 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;
|
||||||
@ -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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -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("");
|
||||||
}}
|
}}
|
||||||
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user