Add description
This commit is contained in:
parent
afb3bfd681
commit
c281d5a6a7
@ -1,66 +1,70 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
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;
|
||||
|
||||
public AuthController(IUserRepository userRepository)
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public AuthController(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register(
|
||||
[FromBody] RegisterUserRequest request)
|
||||
{
|
||||
var user = await _userRepository.Register(
|
||||
request.Email,
|
||||
request.Password);
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register([FromBody] RegisterUserRequest request)
|
||||
{
|
||||
var user = await _userRepository.Register(request.Email, request.Password);
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
[HttpGet("login")]
|
||||
public async Task<IActionResult> Login([FromQuery] string returnUrl)
|
||||
[HttpGet("login")]
|
||||
public async Task<IActionResult> Login([FromQuery] string returnUrl)
|
||||
{
|
||||
var props = new AuthenticationProperties
|
||||
{
|
||||
var props = new AuthenticationProperties
|
||||
RedirectUri = Url.Action(nameof(Callback)),
|
||||
Items =
|
||||
{
|
||||
RedirectUri = Url.Action(nameof(Callback)),
|
||||
Items =
|
||||
{
|
||||
{"returnUrl", returnUrl}
|
||||
"returnUrl", returnUrl
|
||||
}
|
||||
};
|
||||
return Challenge(props);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Callback()
|
||||
{
|
||||
// read external identity from the temporary cookie
|
||||
var result =
|
||||
await HttpContext.AuthenticateAsync("oidc");
|
||||
if (result?.Succeeded != true)
|
||||
{
|
||||
throw new Exception("External authentication error");
|
||||
}
|
||||
};
|
||||
return Challenge(props);
|
||||
}
|
||||
|
||||
var returnUrl = result.Properties?.Items["returnUrl"] ?? "~/";
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Callback()
|
||||
{
|
||||
// read external identity from the temporary cookie
|
||||
var result =
|
||||
await HttpContext.AuthenticateAsync("oidc");
|
||||
if (result?.Succeeded != true)
|
||||
throw new Exception("External authentication error");
|
||||
|
||||
public record RegisterUserRequest
|
||||
{
|
||||
[Required] public string Email { get; init; }
|
||||
[Required] public string Password { get; init; }
|
||||
}
|
||||
var returnUrl = result.Properties?.Items["returnUrl"] ?? "~/";
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
|
||||
public record RegisterUserRequest
|
||||
{
|
||||
[Required]
|
||||
public string Email { get; init; }
|
||||
|
||||
[Required]
|
||||
public string Password { get; init; }
|
||||
}
|
||||
}
|
@ -19,25 +19,37 @@ public class TodosController : ControllerBase
|
||||
|
||||
[HttpPost]
|
||||
[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") ??
|
||||
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]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetTodos() =>
|
||||
Ok(await _todoRepository.GetTodosAsync());
|
||||
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetTodos()
|
||||
{
|
||||
return Ok(await _todoRepository.GetTodosAsync());
|
||||
}
|
||||
|
||||
[HttpGet("not-done")]
|
||||
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetNotDoneTodos() =>
|
||||
Ok(await _todoRepository.GetNotDoneTodos());
|
||||
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>>
|
||||
GetNotDoneTodos()
|
||||
{
|
||||
return Ok(await _todoRepository.GetNotDoneTodos());
|
||||
}
|
||||
|
||||
public record CreateTodoRequest
|
||||
{
|
||||
[Required] public string Title { get; init; }
|
||||
[Required]
|
||||
public string Title { get; init; }
|
||||
}
|
||||
}
|
@ -5,14 +5,17 @@ namespace Todo.Api.Hubs.Models;
|
||||
public record TodoResponse
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; init; }
|
||||
|
||||
public string Id { get; init; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; init; }
|
||||
|
||||
public string Title { get; init; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public bool Status { get; init; }
|
||||
|
||||
[JsonPropertyName("project")]
|
||||
public string Project { get; set; }
|
||||
public string? Project { get; init; }
|
||||
}
|
@ -5,14 +5,17 @@ namespace Todo.Api.Hubs.Models;
|
||||
public record UpdateTodoRequest
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; init; }
|
||||
|
||||
public string Id { get; init; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; init; }
|
||||
|
||||
public string Title { get; init; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public bool Status { get; init; }
|
||||
|
||||
[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 MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -9,17 +7,28 @@ using Todo.Core.Application.Commands.Todo;
|
||||
using Todo.Core.Application.Services.UserConnectionStore;
|
||||
using Todo.Core.Interfaces.Persistence;
|
||||
using Todo.Core.Interfaces.User;
|
||||
using Todo.Infrastructure;
|
||||
|
||||
namespace Todo.Api.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class TodoHub : Hub
|
||||
{
|
||||
private readonly ITodoRepository _todoRepository;
|
||||
private readonly IUserConnectionStore _userConnectionStore;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
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()
|
||||
{
|
||||
@ -38,19 +47,10 @@ public class TodoHub : Hub
|
||||
return base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
|
||||
public TodoHub(
|
||||
ITodoRepository todoRepository,
|
||||
IUserConnectionStore userConnectionStore,
|
||||
ICurrentUserService currentUserService,
|
||||
IMediator mediator)
|
||||
{
|
||||
_todoRepository = todoRepository;
|
||||
_userConnectionStore = userConnectionStore;
|
||||
_currentUserService = currentUserService;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task CreateTodo(string todoTitle, string? projectName)
|
||||
public async Task CreateTodo(
|
||||
string todoTitle,
|
||||
string? projectName,
|
||||
string? description)
|
||||
{
|
||||
if (todoTitle is null)
|
||||
throw new ArgumentException("title cannot be null");
|
||||
@ -58,7 +58,11 @@ public class TodoHub : Hub
|
||||
//var userId = GetUserId();
|
||||
|
||||
//var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName, userId);
|
||||
await _mediator.Send(new CreateTodoCommand(todoTitle, projectName));
|
||||
await _mediator.Send(
|
||||
new CreateTodoCommand(
|
||||
todoTitle,
|
||||
projectName,
|
||||
description));
|
||||
|
||||
await GetInboxTodos();
|
||||
}
|
||||
@ -67,7 +71,10 @@ public class TodoHub : Hub
|
||||
public async Task UpdateTodo(string todoId, bool todoStatus)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
await _todoRepository.UpdateTodoStatus(todoId, todoStatus, userId);
|
||||
await _todoRepository.UpdateTodoStatus(
|
||||
todoId,
|
||||
todoStatus,
|
||||
userId);
|
||||
|
||||
await GetInboxTodos();
|
||||
}
|
||||
@ -75,78 +82,115 @@ public class TodoHub : Hub
|
||||
public async Task GetTodos()
|
||||
{
|
||||
var todos = await _todoRepository.GetTodosAsync();
|
||||
var serializedTodos = JsonSerializer.Serialize(todos
|
||||
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.Project })
|
||||
.ToList());
|
||||
var serializedTodos = JsonSerializer.Serialize(
|
||||
todos
|
||||
.Select(
|
||||
t => new TodoResponse
|
||||
{
|
||||
Id = t.Id,
|
||||
Title = t.Title,
|
||||
Status = t.Status,
|
||||
Project = t.Project,
|
||||
Description = t.Description
|
||||
})
|
||||
.ToList());
|
||||
|
||||
await RunOnUserConnections(async (connections) =>
|
||||
await Clients.Clients(connections).SendAsync("todos", serializedTodos));
|
||||
await RunOnUserConnections(
|
||||
async connections =>
|
||||
await Clients.Clients(connections)
|
||||
.SendAsync("todos", serializedTodos));
|
||||
}
|
||||
|
||||
public async Task GetInboxTodos()
|
||||
{
|
||||
var todos = await _todoRepository.GetNotDoneTodos();
|
||||
var serializedTodos = JsonSerializer.Serialize(todos
|
||||
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.Project })
|
||||
.ToList());
|
||||
var serializedTodos = JsonSerializer.Serialize(
|
||||
todos
|
||||
.Select(
|
||||
t => new TodoResponse
|
||||
{
|
||||
Id = t.Id,
|
||||
Title = t.Title,
|
||||
Status = t.Status,
|
||||
Project = t.Project,
|
||||
Description = t.Description
|
||||
})
|
||||
.ToList());
|
||||
|
||||
await RunOnUserConnections(async (connections) =>
|
||||
await Clients.Clients(connections).SendAsync("getInboxTodos", serializedTodos));
|
||||
await RunOnUserConnections(
|
||||
async connections =>
|
||||
await Clients.Clients(connections)
|
||||
.SendAsync("getInboxTodos", serializedTodos));
|
||||
}
|
||||
|
||||
public async Task GetTodo(string todoId)
|
||||
{
|
||||
var todo = await _todoRepository.GetTodoByIdAsync(todoId);
|
||||
var serializedTodo = JsonSerializer.Serialize(new TodoResponse()
|
||||
{
|
||||
Id = todo.Id,
|
||||
Project = todo.Project,
|
||||
Status = todo.Status,
|
||||
Title = todo.Title,
|
||||
});
|
||||
var serializedTodo = JsonSerializer.Serialize(
|
||||
new TodoResponse
|
||||
{
|
||||
Id = todo.Id,
|
||||
Project = todo.Project,
|
||||
Status = todo.Status,
|
||||
Title = todo.Title,
|
||||
Description = todo.Description
|
||||
});
|
||||
|
||||
await RunOnUserConnections(async (connections) =>
|
||||
await Clients.Clients(connections).SendAsync("getTodo", serializedTodo));
|
||||
await RunOnUserConnections(
|
||||
async connections =>
|
||||
await Clients.Clients(connections)
|
||||
.SendAsync("getTodo", serializedTodo));
|
||||
}
|
||||
|
||||
public async Task ReplaceTodo(string updateTodoRequest)
|
||||
{
|
||||
var updateTodo = JsonSerializer.Deserialize<UpdateTodoRequest>(updateTodoRequest);
|
||||
var updateTodo =
|
||||
JsonSerializer.Deserialize<UpdateTodoRequest>(updateTodoRequest);
|
||||
if (updateTodo is null)
|
||||
throw new InvalidOperationException("Could not parse invalid updateTodo");
|
||||
|
||||
var userId = GetUserId();
|
||||
|
||||
var updatedTodo = await _todoRepository.UpdateTodoAsync(new Core.Entities.Todo()
|
||||
{
|
||||
Id = updateTodo.Id,
|
||||
Project = updateTodo.Project,
|
||||
Status = updateTodo.Status,
|
||||
Title = updateTodo.Title,
|
||||
AuthorId = userId
|
||||
});
|
||||
var updatedTodo = await _todoRepository.UpdateTodoAsync(
|
||||
new Core.Entities.Todo
|
||||
{
|
||||
Id = updateTodo.Id,
|
||||
Project = updateTodo.Project,
|
||||
Status = updateTodo.Status,
|
||||
Title = updateTodo.Title,
|
||||
AuthorId = userId,
|
||||
Description = updateTodo.Description
|
||||
});
|
||||
|
||||
var serializedTodo = JsonSerializer.Serialize(new TodoResponse()
|
||||
{
|
||||
Id = updatedTodo.Id,
|
||||
Project = updatedTodo.Project,
|
||||
Status = updatedTodo.Status,
|
||||
Title = updatedTodo.Title,
|
||||
});
|
||||
var serializedTodo = JsonSerializer.Serialize(
|
||||
new TodoResponse
|
||||
{
|
||||
Id = updatedTodo.Id,
|
||||
Project = updatedTodo.Project,
|
||||
Status = updatedTodo.Status,
|
||||
Title = updatedTodo.Title,
|
||||
Description = updatedTodo.Description
|
||||
});
|
||||
|
||||
await RunOnUserConnections(async (connections) =>
|
||||
await Clients.Clients(connections).SendAsync("getTodo", serializedTodo));
|
||||
await RunOnUserConnections(
|
||||
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 connections = await _userConnectionStore.GetConnectionsAsync(userId);
|
||||
var connections =
|
||||
await _userConnectionStore.GetConnectionsAsync(userId);
|
||||
|
||||
await action(connections);
|
||||
}
|
||||
|
||||
private string GetUserId() =>
|
||||
_currentUserService.GetUserId() ??
|
||||
throw new InvalidOperationException("User id was invalid. Something has gone terribly wrong");
|
||||
private string GetUserId()
|
||||
{
|
||||
return _currentUserService.GetUserId() ??
|
||||
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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
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) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
|
||||
private static IHostBuilder CreateHostBuilder(string[] args)
|
||||
{
|
||||
return Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@
|
||||
"MONGODB__Host": "localhost",
|
||||
"MONGODB__Port": "27017",
|
||||
"GITEA__Url": "https://git.front.kjuulh.io",
|
||||
"GITEA__ClientId": "6982ef4f-cfc1-431c-a442-fad98355a059",
|
||||
"GITEA__ClientId": "6982ef4f-cfc1-431c-a442-fad98355a059",
|
||||
"GITEA__ClientSecret": "hXUrUz5xPhC7IE3dQKft9lHboBEwhNC8yFjSzKgF9Nyr"
|
||||
}
|
||||
}
|
||||
|
@ -10,15 +10,15 @@ namespace Todo.Api.Publishers;
|
||||
|
||||
public class TodoPublisher : ITodoPublisher
|
||||
{
|
||||
private readonly IHubContext<TodoHub> _hubContext;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IUserConnectionStore _userConnectionStore;
|
||||
private readonly IHubContext<TodoHub> _hubContext;
|
||||
private readonly ILogger<TodoPublisher> _logger;
|
||||
private readonly IUserConnectionStore _userConnectionStore;
|
||||
|
||||
public TodoPublisher(
|
||||
IHubContext<TodoHub> hubContext,
|
||||
ICurrentUserService currentUserService,
|
||||
IUserConnectionStore userConnectionStore,
|
||||
ICurrentUserService currentUserService,
|
||||
IUserConnectionStore userConnectionStore,
|
||||
ILogger<TodoPublisher> logger)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
@ -26,17 +26,24 @@ public class TodoPublisher : ITodoPublisher
|
||||
_userConnectionStore = userConnectionStore;
|
||||
_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 connections = await _userConnectionStore.GetConnectionsAsync(userId);
|
||||
|
||||
await _hubContext
|
||||
.Clients
|
||||
.Clients(connections)
|
||||
.SendAsync("todoCreated", todoId , cancellationToken);
|
||||
|
||||
_logger.LogInformation("todo created {TodoId}", todoId);
|
||||
var userId = _currentUserService.GetUserId() ??
|
||||
throw new InvalidOperationException("Cannot proceed without user");
|
||||
var connections =
|
||||
await _userConnectionStore.GetConnectionsAsync(userId);
|
||||
|
||||
await _hubContext
|
||||
.Clients
|
||||
.Clients(connections)
|
||||
.SendAsync(
|
||||
"todoCreated",
|
||||
todoId,
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogInformation("todo created {TodoId}", todoId);
|
||||
}
|
||||
}
|
@ -8,10 +8,14 @@ public class HttpContextCurrentUserService : ICurrentUserService
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public HttpContextCurrentUserService(IHttpContextAccessor httpContextAccessor)
|
||||
public HttpContextCurrentUserService(
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public string? GetUserId() => _httpContextAccessor.HttpContext?.User.FindFirstValue("sub");
|
||||
|
||||
public string? GetUserId()
|
||||
{
|
||||
return _httpContextAccessor.HttpContext?.User.FindFirstValue("sub");
|
||||
}
|
||||
}
|
@ -10,112 +10,134 @@ using Microsoft.OpenApi.Models;
|
||||
using Todo.Api.Hubs;
|
||||
using Todo.Api.Publishers;
|
||||
using Todo.Api.Services;
|
||||
using Todo.Core;
|
||||
using Todo.Core.Interfaces.Publisher;
|
||||
using Todo.Core.Interfaces.User;
|
||||
using Todo.Infrastructure;
|
||||
using Todo.Persistence;
|
||||
using Todo.Persistence.Mongo;
|
||||
using Todo.Core;
|
||||
using Todo.Core.Interfaces.Publisher;
|
||||
|
||||
namespace Todo.Api
|
||||
namespace Todo.Api;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
public class Startup
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers();
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddCors(options =>
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers();
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddCors(
|
||||
options =>
|
||||
{
|
||||
options.AddDefaultPolicy(builder =>
|
||||
builder.WithOrigins("http://localhost:3000", "https://todo.front.kjuulh.io")
|
||||
.AllowCredentials()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod());
|
||||
options.AddDefaultPolicy(
|
||||
builder =>
|
||||
builder.WithOrigins(
|
||||
"http://localhost:3000",
|
||||
"https://todo.front.kjuulh.io")
|
||||
.AllowCredentials()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod());
|
||||
});
|
||||
|
||||
services.AddCore();
|
||||
services.AddScoped<ICurrentUserService, HttpContextCurrentUserService>();
|
||||
services.AddScoped<ITodoPublisher, TodoPublisher>();
|
||||
services.AddCore();
|
||||
services
|
||||
.AddScoped<ICurrentUserService, HttpContextCurrentUserService>();
|
||||
services.AddScoped<ITodoPublisher, TodoPublisher>();
|
||||
|
||||
services.AddSwaggerGen(c =>
|
||||
services.AddSwaggerGen(
|
||||
c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo.Api", Version = "v1" });
|
||||
c.SwaggerDoc(
|
||||
"v1",
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = "Todo.Api",
|
||||
Version = "v1"
|
||||
});
|
||||
});
|
||||
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
|
||||
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
|
||||
|
||||
services.AddInfrastructure(Configuration);
|
||||
services.AddInfrastructure(Configuration);
|
||||
|
||||
services.AddPersistence(Configuration, out var mongoDbOptions);
|
||||
services
|
||||
.AddHealthChecks()
|
||||
.AddMongoDb(MongoDbConnectionHandler.FormatConnectionString(mongoDbOptions));
|
||||
services.AddSignalR();
|
||||
services.AddPersistence(Configuration, out var mongoDbOptions);
|
||||
services
|
||||
.AddHealthChecks()
|
||||
.AddMongoDb(
|
||||
MongoDbConnectionHandler
|
||||
.FormatConnectionString(mongoDbOptions));
|
||||
services.AddSignalR();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
app.MigrateMongoDb();
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(
|
||||
c => c.SwaggerEndpoint(
|
||||
"/swagger/v1/swagger.json",
|
||||
"Todo.Api v1"));
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
app.MigrateMongoDb();
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
app.UseInfrastructure();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Todo.Api v1"));
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
app.UseInfrastructure();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
app.UseEndpoints(
|
||||
endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapHub<TodoHub>("/hubs/todo");
|
||||
endpoints.MapHealthChecks("/health/live", new HealthCheckOptions()
|
||||
{
|
||||
ResponseWriter = async (context, report) =>
|
||||
endpoints.MapHealthChecks(
|
||||
"/health/live",
|
||||
new HealthCheckOptions
|
||||
{
|
||||
var response = new HealthCheckResponse()
|
||||
ResponseWriter = async (context, report) =>
|
||||
{
|
||||
Status = report.Status.ToString(),
|
||||
HealthChecks = report.Entries.Select(x => new IndividualHealthCheckResponse
|
||||
var response = new HealthCheckResponse
|
||||
{
|
||||
Component = x.Key,
|
||||
Status = x.Value.Status.ToString(),
|
||||
Description = x.Value.Description
|
||||
}),
|
||||
HealthCheckDuration = report.TotalDuration
|
||||
};
|
||||
await context.Response.WriteAsJsonAsync(response);
|
||||
}
|
||||
});
|
||||
Status = report.Status.ToString(),
|
||||
HealthChecks = report.Entries.Select(
|
||||
x => new IndividualHealthCheckResponse
|
||||
{
|
||||
Component = x.Key,
|
||||
Status = x.Value.Status.ToString(),
|
||||
Description = x.Value.Description
|
||||
}),
|
||||
HealthCheckDuration = report.TotalDuration
|
||||
};
|
||||
await context.Response.WriteAsJsonAsync(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class HealthCheckResponse
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public IEnumerable<IndividualHealthCheckResponse> HealthChecks { get; set; }
|
||||
public TimeSpan HealthCheckDuration { get; set; }
|
||||
}
|
||||
private class HealthCheckResponse
|
||||
{
|
||||
public string Status { get; set; }
|
||||
|
||||
private class IndividualHealthCheckResponse
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public string Component { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
public IEnumerable<IndividualHealthCheckResponse> HealthChecks { get; set; }
|
||||
|
||||
public TimeSpan HealthCheckDuration { get; set; }
|
||||
}
|
||||
|
||||
private class IndividualHealthCheckResponse
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public string Component { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
@ -7,13 +7,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Todo.Infrastructure\Todo.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj" />
|
||||
<ProjectReference Include="..\Todo.Persistence\Todo.Persistence.csproj" />
|
||||
<ProjectReference Include="..\Todo.Infrastructure\Todo.Infrastructure.csproj"/>
|
||||
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj"/>
|
||||
<ProjectReference Include="..\Todo.Persistence\Todo.Persistence.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -6,30 +6,44 @@ using Todo.Core.Interfaces.User;
|
||||
|
||||
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>
|
||||
{
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly ITodoRepository _todoRepository;
|
||||
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;
|
||||
_todoRepository = todoRepository;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<string> Handle(CreateTodoCommand request, CancellationToken cancellationToken)
|
||||
public async Task<string> Handle(
|
||||
CreateTodoCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = _currentUserService.GetUserId();
|
||||
if (userId is null)
|
||||
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;
|
||||
}
|
||||
|
@ -2,14 +2,12 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Todo.Core.Application.Services.UserConnectionStore;
|
||||
using Todo.Core.Interfaces.Publisher;
|
||||
using Todo.Core.Interfaces.User;
|
||||
|
||||
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>
|
||||
{
|
||||
@ -20,9 +18,13 @@ public record TodoCreated([property: JsonPropertyName("todoId")] string TodoId)
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,5 +4,5 @@ namespace Todo.Core.Interfaces.Publisher;
|
||||
|
||||
public interface ITodoPublisher
|
||||
{
|
||||
Task Publish(string todoId, CancellationToken cancellationToken = new ());
|
||||
Task Publish(string todoId, CancellationToken cancellationToken = new());
|
||||
}
|
@ -5,4 +5,4 @@ public interface IUserConnectionStore
|
||||
Task AddAsync(string userId, string connectionId);
|
||||
Task RemoveAsync(string userId, string connectionId);
|
||||
Task<IEnumerable<string>> GetConnectionsAsync(string userId);
|
||||
}
|
||||
}
|
@ -10,5 +10,8 @@ namespace Todo.Core;
|
||||
|
||||
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 Title { get; init; }
|
||||
public bool Status { get; init; }
|
||||
public string Project { get; init; }
|
||||
public string? Project { 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 Email { get; set; }
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
}
|
@ -1,11 +1,20 @@
|
||||
|
||||
namespace Todo.Core.Interfaces.Persistence;
|
||||
|
||||
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 UpdateTodoStatus(string todoId, bool todoStatus, string userId);
|
||||
|
||||
Task UpdateTodoStatus(
|
||||
string todoId,
|
||||
bool todoStatus,
|
||||
string userId);
|
||||
|
||||
Task<IEnumerable<Entities.Todo>> GetNotDoneTodos();
|
||||
Task<Entities.Todo> GetTodoByIdAsync(string todoId);
|
||||
Task<Entities.Todo> UpdateTodoAsync(Entities.Todo todo);
|
||||
|
@ -1,5 +1,3 @@
|
||||
using Todo.Core.Entities;
|
||||
|
||||
namespace Todo.Core.Interfaces.Persistence;
|
||||
|
||||
public interface IUserRepository
|
||||
|
@ -4,18 +4,18 @@
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Application\Services" />
|
||||
<Folder Include="Application\Services"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Mediatr" Version="9.0.0" />
|
||||
<PackageReference Include="Mediatr.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Mediatr" Version="9.0.0"/>
|
||||
<PackageReference Include="Mediatr.Extensions.Microsoft.DependencyInjection" Version="9.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -10,7 +10,9 @@ namespace Todo.Infrastructure;
|
||||
|
||||
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 giteaOptions = configuration.GetRequiredSection("GITEA");
|
||||
@ -21,35 +23,49 @@ public static class DependencyInjection
|
||||
.Bind(giteaOptions)
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
services.AddSingleton<IUserConnectionStore, InMemoryUserConnectionStore>();
|
||||
services
|
||||
.AddSingleton<IUserConnectionStore, InMemoryUserConnectionStore>();
|
||||
|
||||
return services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = "Cookies";
|
||||
options.DefaultChallengeScheme = "oidc";
|
||||
})
|
||||
return services.AddAuthentication(
|
||||
options =>
|
||||
{
|
||||
options.DefaultScheme = "Cookies";
|
||||
options.DefaultChallengeScheme = "oidc";
|
||||
})
|
||||
.AddCookie("Cookies")
|
||||
.AddOpenIdConnect("oidc", options =>
|
||||
{
|
||||
options.Authority = giteaAuthOptions.Url;
|
||||
options.ClientId = giteaAuthOptions.ClientId;
|
||||
options.ClientSecret = giteaAuthOptions.ClientSecret;
|
||||
options.ResponseType = "code";
|
||||
.AddOpenIdConnect(
|
||||
"oidc",
|
||||
options =>
|
||||
{
|
||||
options.Authority = giteaAuthOptions.Url;
|
||||
options.ClientId = giteaAuthOptions.ClientId;
|
||||
options.ClientSecret = giteaAuthOptions.ClientSecret;
|
||||
options.ResponseType = "code";
|
||||
|
||||
options.SaveTokens = true;
|
||||
}).Services;
|
||||
options.SaveTokens = true;
|
||||
})
|
||||
.Services;
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) => app.UseCookiePolicy(
|
||||
new CookiePolicyOptions
|
||||
{
|
||||
Secure = CookieSecurePolicy.Always
|
||||
});
|
||||
public static IApplicationBuilder UseInfrastructure(
|
||||
this IApplicationBuilder app)
|
||||
{
|
||||
return app.UseCookiePolicy(
|
||||
new CookiePolicyOptions
|
||||
{
|
||||
Secure = CookieSecurePolicy.Always
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class GiteaAuthOptions
|
||||
{
|
||||
[Required] public string Url { get; set; }
|
||||
[Required] public string ClientId { get; init; }
|
||||
[Required] public string ClientSecret { get; init; }
|
||||
[Required]
|
||||
public string Url { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ClientId { get; init; }
|
||||
|
||||
[Required]
|
||||
public string ClientSecret { get; init; }
|
||||
}
|
@ -5,17 +5,17 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj" />
|
||||
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -3,9 +3,10 @@ using Todo.Core.Application.Services.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)
|
||||
{
|
||||
@ -27,7 +28,10 @@ class InMemoryUserConnectionStore : IUserConnectionStore
|
||||
public Task<IEnumerable<string>> GetConnectionsAsync(string userId)
|
||||
{
|
||||
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)
|
||||
@ -39,12 +43,10 @@ class InMemoryUserConnectionStore : IUserConnectionStore
|
||||
|
||||
// If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers).
|
||||
if (existingUserConnectionIds?.Count == 0)
|
||||
{
|
||||
// if there are no connections for the user,
|
||||
// just delete the userName key from the ConnectedUsers concurent dictionary
|
||||
ConnectedUsers.TryRemove(userId, out _);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,32 +4,32 @@ global using System.Linq;
|
||||
global using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Todo.Core.Interfaces.Persistence;
|
||||
using Todo.Persistence.Mongo;
|
||||
using Todo.Persistence.Mongo.Repositories;
|
||||
|
||||
namespace Todo.Persistence
|
||||
namespace Todo.Persistence;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static class DependencyInjection
|
||||
public static IServiceCollection AddPersistence(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
out MongoDbOptions mongoDbOptions)
|
||||
{
|
||||
public static IServiceCollection AddPersistence(this IServiceCollection services, IConfiguration configuration,
|
||||
out MongoDbOptions mongoDbOptions)
|
||||
{
|
||||
var exportableMongoDbOptions = new MongoDbOptions();
|
||||
var options = configuration.GetRequiredSection("MONGODB");
|
||||
options.Bind(exportableMongoDbOptions);
|
||||
mongoDbOptions = exportableMongoDbOptions;
|
||||
var exportableMongoDbOptions = new MongoDbOptions();
|
||||
var options = configuration.GetRequiredSection("MONGODB");
|
||||
options.Bind(exportableMongoDbOptions);
|
||||
mongoDbOptions = exportableMongoDbOptions;
|
||||
|
||||
services
|
||||
.AddOptions<MongoDbOptions>()
|
||||
.Bind(options)
|
||||
.ValidateDataAnnotations();
|
||||
services
|
||||
.AddOptions<MongoDbOptions>()
|
||||
.Bind(options)
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
return services
|
||||
.AddSingleton<MongoDbConnectionHandler>()
|
||||
.AddScoped<IUserRepository, UserRepository>()
|
||||
.AddScoped<ITodoRepository, TodoRepository>();
|
||||
}
|
||||
return services
|
||||
.AddSingleton<MongoDbConnectionHandler>()
|
||||
.AddScoped<IUserRepository, UserRepository>()
|
||||
.AddScoped<ITodoRepository, TodoRepository>();
|
||||
}
|
||||
}
|
@ -3,33 +3,42 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using MongoDB.Driver;
|
||||
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 () =>
|
||||
{
|
||||
var connectionHandler = app.ApplicationServices.GetRequiredService<MongoDbConnectionHandler>();
|
||||
var database = connectionHandler.CreateDatabaseConnection();
|
||||
Task.Run(
|
||||
async () =>
|
||||
{
|
||||
var connectionHandler = app.ApplicationServices
|
||||
.GetRequiredService<MongoDbConnectionHandler>();
|
||||
var database = connectionHandler.CreateDatabaseConnection();
|
||||
|
||||
await CreateIndexes(database);
|
||||
}).Wait(10000);
|
||||
await CreateIndexes(database);
|
||||
})
|
||||
.Wait(10000);
|
||||
|
||||
return app;
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task CreateIndexes(IMongoDatabase mongoDatabase)
|
||||
{
|
||||
Console.WriteLine("Running: CreateIndexes");
|
||||
var collection = mongoDatabase.GetCollection<MongoUser>("users");
|
||||
var indexKeysDefinition = Builders<MongoUser>.IndexKeys.Ascending(user => user.Email);
|
||||
var indexModel =
|
||||
new CreateIndexModel<MongoUser>(indexKeysDefinition, new CreateIndexOptions() { Unique = true });
|
||||
private static async Task CreateIndexes(IMongoDatabase mongoDatabase)
|
||||
{
|
||||
Console.WriteLine("Running: CreateIndexes");
|
||||
var collection = mongoDatabase.GetCollection<MongoUser>("users");
|
||||
var indexKeysDefinition =
|
||||
Builders<MongoUser>.IndexKeys.Ascending(user => user.Email);
|
||||
var indexModel =
|
||||
new CreateIndexModel<MongoUser>(
|
||||
indexKeysDefinition,
|
||||
new CreateIndexOptions
|
||||
{
|
||||
Unique = true
|
||||
});
|
||||
|
||||
await collection.Indexes.CreateOneAsync(indexModel);
|
||||
Console.WriteLine("Finished: CreateIndexes");
|
||||
}
|
||||
await collection.Indexes.CreateOneAsync(indexModel);
|
||||
Console.WriteLine("Finished: CreateIndexes");
|
||||
}
|
||||
}
|
@ -7,7 +7,8 @@ public class MongoDbConnectionHandler
|
||||
{
|
||||
private readonly IOptionsMonitor<MongoDbOptions> _optionsMonitor;
|
||||
|
||||
public MongoDbConnectionHandler(IOptionsMonitor<MongoDbOptions> optionsMonitor)
|
||||
public MongoDbConnectionHandler(
|
||||
IOptionsMonitor<MongoDbOptions> optionsMonitor)
|
||||
{
|
||||
_optionsMonitor = optionsMonitor;
|
||||
}
|
||||
@ -19,7 +20,8 @@ public class MongoDbConnectionHandler
|
||||
return CreateConnectionFromOptions(options);
|
||||
}
|
||||
|
||||
private static IMongoDatabase CreateConnectionFromOptions(MongoDbOptions options)
|
||||
private static IMongoDatabase CreateConnectionFromOptions(
|
||||
MongoDbOptions options)
|
||||
{
|
||||
var conn = new MongoClient(FormatConnectionString(options));
|
||||
var database = conn.GetDatabase(options.Database);
|
||||
@ -27,6 +29,9 @@ public class MongoDbConnectionHandler
|
||||
return database;
|
||||
}
|
||||
|
||||
public static string FormatConnectionString(MongoDbOptions options) =>
|
||||
$"mongodb://{options.Username}:{options.Password}@{options.Host}:{options.Port}";
|
||||
public static string FormatConnectionString(MongoDbOptions options)
|
||||
{
|
||||
return
|
||||
$"mongodb://{options.Username}:{options.Password}@{options.Host}:{options.Port}";
|
||||
}
|
||||
}
|
@ -6,9 +6,18 @@ public class MongoDbOptions
|
||||
{
|
||||
public const string MongoDb = "MongoDb";
|
||||
|
||||
[Required] public string Username { get; set; }
|
||||
[Required] public string Password { get; set; }
|
||||
[Required] public string Host { get; set; }
|
||||
[Required] public string Port { get; set; }
|
||||
[Required] public string Database { get; set; }
|
||||
[Required]
|
||||
public string Username { get; set; }
|
||||
|
||||
[Required]
|
||||
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)]
|
||||
public string Id { get; init; }
|
||||
|
||||
[BsonRequired] public string Title { get; init; }
|
||||
[BsonRequired] public bool Status { get; set; }
|
||||
public string ProjectName { get; set; } = String.Empty;
|
||||
[BsonRequired]
|
||||
public string Title { get; init; }
|
||||
|
||||
public string? Description { get; init; }
|
||||
|
||||
[BsonRequired]
|
||||
public bool Status { get; set; }
|
||||
|
||||
public string? ProjectName { get; set; } = string.Empty;
|
||||
public string AuthorId { get; set; }
|
||||
}
|
@ -1,15 +1,17 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Todo.Persistence.Mongo.Repositories.Dtos
|
||||
{
|
||||
public record MongoUser
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string Id { get; init; }
|
||||
namespace Todo.Persistence.Mongo.Repositories.Dtos;
|
||||
|
||||
[BsonRequired] public string Email { get; init; }
|
||||
[BsonRequired] public string Password { get; set; }
|
||||
}
|
||||
public record MongoUser
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string Id { get; init; }
|
||||
|
||||
[BsonRequired]
|
||||
public string Email { get; init; }
|
||||
|
||||
[BsonRequired]
|
||||
public string Password { get; set; }
|
||||
}
|
@ -15,12 +15,28 @@ public class TodoRepository : ITodoRepository
|
||||
_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);
|
||||
return new Core.Entities.Todo()
|
||||
{ Id = todo.Id, Title = todo.Title, Status = false, Project = todo.ProjectName };
|
||||
return new Core.Entities.Todo
|
||||
{
|
||||
Id = todo.Id,
|
||||
Title = todo.Title,
|
||||
Status = false,
|
||||
Project = todo.ProjectName,
|
||||
Description = todo.Description
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.Todo>> GetTodosAsync()
|
||||
@ -28,14 +44,26 @@ public class TodoRepository : ITodoRepository
|
||||
var todos = await _todosCollection.FindAsync(_ => true);
|
||||
return todos
|
||||
.ToEnumerable()
|
||||
.Select(t =>
|
||||
new Core.Entities.Todo() { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.ProjectName });
|
||||
.Select(
|
||||
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
|
||||
.UpdateOneAsync(t => t.Id == todoId && t.AuthorId == userId,
|
||||
.UpdateOneAsync(
|
||||
t => t.Id == todoId && t.AuthorId == userId,
|
||||
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 todo = await todoCursor.FirstOrDefaultAsync();
|
||||
|
||||
return new Core.Entities.Todo()
|
||||
return new Core.Entities.Todo
|
||||
{
|
||||
Id = todo.Id,
|
||||
Project = todo.ProjectName,
|
||||
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()
|
||||
{
|
||||
Id = todo.Id,
|
||||
Status = todo.Status,
|
||||
Title = todo.Title,
|
||||
ProjectName = todo.Project,
|
||||
AuthorId = todo.AuthorId
|
||||
});
|
||||
var updatedTodo = await _todosCollection.FindOneAndReplaceAsync(
|
||||
f => f.Id == todo.Id,
|
||||
new MongoTodo
|
||||
{
|
||||
Id = todo.Id,
|
||||
Status = todo.Status,
|
||||
Title = todo.Title,
|
||||
ProjectName = todo.Project,
|
||||
AuthorId = todo.AuthorId,
|
||||
Description = todo.Description
|
||||
});
|
||||
|
||||
return new Core.Entities.Todo()
|
||||
return new Core.Entities.Todo
|
||||
{
|
||||
Id = updatedTodo.Id,
|
||||
Project = updatedTodo.ProjectName,
|
||||
Status = updatedTodo.Status,
|
||||
Title = updatedTodo.Title,
|
||||
AuthorId = updatedTodo.AuthorId
|
||||
AuthorId = updatedTodo.AuthorId,
|
||||
Description = updatedTodo.Description
|
||||
};
|
||||
}
|
||||
}
|
@ -3,23 +3,30 @@ using Todo.Core.Entities;
|
||||
using Todo.Core.Interfaces.Persistence;
|
||||
using Todo.Persistence.Mongo.Repositories.Dtos;
|
||||
|
||||
namespace Todo.Persistence.Mongo.Repositories
|
||||
namespace Todo.Persistence.Mongo.Repositories;
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
{
|
||||
public class UserRepository : IUserRepository
|
||||
private readonly IMongoCollection<MongoUser> _usersCollection;
|
||||
|
||||
public UserRepository(MongoDbConnectionHandler mongoDbConnectionHandler)
|
||||
{
|
||||
private readonly IMongoCollection<MongoUser> _usersCollection;
|
||||
var database = mongoDbConnectionHandler.CreateDatabaseConnection();
|
||||
_usersCollection = database.GetCollection<MongoUser>("users");
|
||||
}
|
||||
|
||||
public UserRepository(MongoDbConnectionHandler mongoDbConnectionHandler)
|
||||
public async Task<User> Register(string email, string password)
|
||||
{
|
||||
var dtoUser = new MongoUser
|
||||
{
|
||||
var database = mongoDbConnectionHandler.CreateDatabaseConnection();
|
||||
_usersCollection = database.GetCollection<MongoUser>("users");
|
||||
}
|
||||
|
||||
public async Task<User> Register(string email, string password)
|
||||
Email = email,
|
||||
Password = password
|
||||
};
|
||||
await _usersCollection.InsertOneAsync(dtoUser);
|
||||
return new User
|
||||
{
|
||||
var dtoUser = new MongoUser() { Email = email, Password = password };
|
||||
await _usersCollection.InsertOneAsync(dtoUser);
|
||||
return new User() { Email = dtoUser.Email, Id = dtoUser.Id };
|
||||
}
|
||||
Email = dtoUser.Email,
|
||||
Id = dtoUser.Id
|
||||
};
|
||||
}
|
||||
}
|
@ -2,20 +2,21 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.13.2" />
|
||||
<PackageReference Include="MongoDB.Driver.Core" Version="2.13.2" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="5.0.1" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.13.2"/>
|
||||
<PackageReference Include="MongoDB.Driver.Core" Version="2.13.2"/>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="5.0.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj" />
|
||||
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -4,7 +4,7 @@ import { PrimaryButton } from "@src/components/common/buttons/primaryButton";
|
||||
import { TodoShortForm } from "@src/components/todos/collapsed/todoShortForm";
|
||||
|
||||
export const AddTodoForm: FC<{
|
||||
onAdd: (todoName: string, project: string) => void;
|
||||
onAdd: (todoName: string, project: string, description: string) => void;
|
||||
onClose: () => void;
|
||||
project: string;
|
||||
}> = ({ onAdd, onClose, ...props }) => {
|
||||
@ -16,7 +16,7 @@ export const AddTodoForm: FC<{
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onAdd(todoName, project);
|
||||
onAdd(todoName, project, todoDescription);
|
||||
setTodoName("");
|
||||
setTodoDescription("");
|
||||
}}
|
||||
|
@ -22,8 +22,8 @@ export function AddTodo(props: { project: string }) {
|
||||
return (
|
||||
<AddTodoForm
|
||||
project={props.project}
|
||||
onAdd={(todoName, project) => {
|
||||
createTodo(todoName, project);
|
||||
onAdd={(todoName, project, description) => {
|
||||
createTodo(todoName, project, description);
|
||||
}}
|
||||
onClose={() => setCollapsed(CollapsedState.collapsed)}
|
||||
/>
|
||||
|
@ -29,7 +29,12 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
|
||||
{props.todo.title}
|
||||
</a>
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex flex-row justify-between items-end">
|
||||
<div>
|
||||
{props.todo.description && (
|
||||
<div className="h-3 w-3 bg-gray-200 dark:bg-gray-900"/>
|
||||
)}
|
||||
</div>
|
||||
{props.displayProject && props.todo.project && (
|
||||
<div className="text-gray-500 text-xs text-right whitespace-nowrap place-self-end">
|
||||
{props.todo.project}
|
||||
|
@ -9,6 +9,7 @@ export const StatusState: { done: Done; notDone: NotDone } = {
|
||||
export interface Todo {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: StatusState;
|
||||
project?: string;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ interface SocketContextProps {
|
||||
inboxTodos: Todo[];
|
||||
getTodos: () => void;
|
||||
getInboxTodos: () => void;
|
||||
createTodo: (todoName: string, project: string) => void;
|
||||
createTodo: (todoName: string, project: string, description: string) => void;
|
||||
updateTodo: (todoId: string, todoStatus: StatusState) => void;
|
||||
getTodoById(todoId: string): void;
|
||||
replaceTodo(todo: Todo): void;
|
||||
@ -24,7 +24,7 @@ export const SocketContext = createContext<SocketContextProps>({
|
||||
inboxTodos: [],
|
||||
getTodos: () => {},
|
||||
getInboxTodos: () => {},
|
||||
createTodo: (todoName, project) => {},
|
||||
createTodo: (todoName, project, description) => {},
|
||||
updateTodo: (todoId, todoStatus) => {},
|
||||
getTodoById(todoId: string) {},
|
||||
replaceTodo(todo: Todo) {},
|
||||
@ -89,8 +89,8 @@ export const SocketProvider: FC = (props) => {
|
||||
getInboxTodos: () => {
|
||||
conn.invoke("GetInboxTodos").catch(console.error);
|
||||
},
|
||||
createTodo: (todoName, project) => {
|
||||
conn.invoke("CreateTodo", todoName, project).catch(console.error);
|
||||
createTodo: (todoName, project, description) => {
|
||||
conn.invoke("CreateTodo", todoName, project, description).catch(console.error);
|
||||
},
|
||||
updateTodo: (todoId, todoStatus) => {
|
||||
conn.invoke("UpdateTodo", todoId, todoStatus).catch(console.error);
|
||||
|
@ -19,8 +19,8 @@ export const useCreateTodo = () => {
|
||||
const socketContext = useContext(SocketContext);
|
||||
|
||||
return {
|
||||
createTodo: (todoName: string, project: string) => {
|
||||
socketContext.createTodo(todoName, project);
|
||||
createTodo: (todoName: string, project: string, description: string) => {
|
||||
socketContext.createTodo(todoName, project, description);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user