diff --git a/src/backend/server/src/Todo.Api/Controllers/ApiController.cs b/src/backend/server/src/Todo.Api/Controllers/ApiController.cs new file mode 100644 index 0000000..4e63c08 --- /dev/null +++ b/src/backend/server/src/Todo.Api/Controllers/ApiController.cs @@ -0,0 +1,11 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace Todo.Api.Controllers; + +[ApiController] +public class ApiController : ControllerBase +{ + public IMediator Mediator => HttpContext.RequestServices.GetRequiredService(); +} \ No newline at end of file diff --git a/src/backend/server/src/Todo.Api/Controllers/AuthController.cs b/src/backend/server/src/Todo.Api/Controllers/AuthController.cs index f2a0d39..9bfa57b 100644 --- a/src/backend/server/src/Todo.Api/Controllers/AuthController.cs +++ b/src/backend/server/src/Todo.Api/Controllers/AuthController.cs @@ -14,10 +14,7 @@ public class AuthController : ControllerBase { private readonly IUserRepository _userRepository; - public AuthController(IUserRepository userRepository) - { - _userRepository = userRepository; - } + public AuthController(IUserRepository userRepository) => _userRepository = userRepository; [HttpPost("register")] public async Task Register( diff --git a/src/backend/server/src/Todo.Api/Controllers/TodosController.cs b/src/backend/server/src/Todo.Api/Controllers/TodosController.cs index 84af365..546ee8e 100644 --- a/src/backend/server/src/Todo.Api/Controllers/TodosController.cs +++ b/src/backend/server/src/Todo.Api/Controllers/TodosController.cs @@ -1,55 +1,54 @@ using System.ComponentModel.DataAnnotations; -using System.Security.Claims; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Todo.Core.Application.Commands.Todo; +using Todo.Core.Application.Queries.Todos; using Todo.Core.Interfaces.Persistence; namespace Todo.Api.Controllers; -[ApiController] [Route("/api/todos")] -public class TodosController : ControllerBase +[Authorize] +public class TodosController : ApiController { private readonly ITodoRepository _todoRepository; - public TodosController(ITodoRepository todoRepository) - { - _todoRepository = todoRepository; - } - - [HttpPost] - [Authorize] - public async Task> 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)); - } - - [HttpGet] - [Authorize] - public async Task>> GetTodos() - { - return Ok(await _todoRepository.GetTodosAsync()); - } - - [HttpGet("not-done")] - public async Task>> - GetNotDoneTodos() - { - return Ok(await _todoRepository.GetNotDoneTodos()); - } + public TodosController(ITodoRepository todoRepository) => _todoRepository = todoRepository; public record CreateTodoRequest { [Required] + [JsonPropertyName("title")] public string Title { get; init; } + + [JsonPropertyName("description")] + public string? Description { get; init; } + + [JsonPropertyName("project")] + public string? Project { get; init; } + + internal CreateTodoCommand To() => new( + Title, + Project, + Description); } + + [HttpPost] + public async Task> CreateTodo( + [FromBody] CreateTodoRequest request) + => Ok(await Mediator.Send(request.To())); + + [HttpGet("{todoId}")] + public async Task> GetTodoById([FromRoute] string todoId) + => await Mediator.Send(new GetTodoByIdQuery(todoId)); + + [HttpGet] + public async Task>> GetTodos() + => Ok(await _todoRepository.GetTodosAsync()); + + [HttpGet("not-done")] + public async Task>> + GetNotDoneTodos() + => Ok(await _todoRepository.GetNotDoneTodos()); } \ No newline at end of file diff --git a/src/backend/server/src/Todo.Api/Hubs/TodoHub.cs b/src/backend/server/src/Todo.Api/Hubs/TodoHub.cs index a377478..b38ffe5 100644 --- a/src/backend/server/src/Todo.Api/Hubs/TodoHub.cs +++ b/src/backend/server/src/Todo.Api/Hubs/TodoHub.cs @@ -188,9 +188,7 @@ public class TodoHub : Hub await action(connections); } - private string GetUserId() - { - return _currentUserService.GetUserId() ?? - throw new InvalidOperationException("User id was invalid. Something has gone terribly wrong"); - } + private string GetUserId() => _currentUserService.GetUserId() ?? + throw new InvalidOperationException( + "User id was invalid. Something has gone terribly wrong"); } \ No newline at end of file diff --git a/src/backend/server/src/Todo.Api/Services/HttpContextCurrentUserService.cs b/src/backend/server/src/Todo.Api/Services/HttpContextCurrentUserService.cs index fa7a472..5b444e6 100644 --- a/src/backend/server/src/Todo.Api/Services/HttpContextCurrentUserService.cs +++ b/src/backend/server/src/Todo.Api/Services/HttpContextCurrentUserService.cs @@ -10,12 +10,7 @@ public class HttpContextCurrentUserService : ICurrentUserService public HttpContextCurrentUserService( IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } + => _httpContextAccessor = httpContextAccessor; - public string? GetUserId() - { - return _httpContextAccessor.HttpContext?.User.FindFirstValue("sub"); - } + public string? GetUserId() => _httpContextAccessor.HttpContext?.User.FindFirstValue("sub"); } \ No newline at end of file diff --git a/src/backend/server/src/Todo.Api/Startup.cs b/src/backend/server/src/Todo.Api/Startup.cs index e31ef48..c3d7b6b 100644 --- a/src/backend/server/src/Todo.Api/Startup.cs +++ b/src/backend/server/src/Todo.Api/Startup.cs @@ -21,10 +21,7 @@ namespace Todo.Api; public class Startup { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + public Startup(IConfiguration configuration) => Configuration = configuration; public IConfiguration Configuration { get; } diff --git a/src/backend/server/src/Todo.Core/Application/Commands/Todo/CreateTodoCommand.cs b/src/backend/server/src/Todo.Core/Application/Commands/Todo/CreateTodoCommand.cs index 6f86737..920b428 100644 --- a/src/backend/server/src/Todo.Core/Application/Commands/Todo/CreateTodoCommand.cs +++ b/src/backend/server/src/Todo.Core/Application/Commands/Todo/CreateTodoCommand.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.Threading; using MediatR; using Todo.Core.Application.Notifications.Todo; @@ -7,7 +8,7 @@ using Todo.Core.Interfaces.User; namespace Todo.Core.Application.Commands.Todo; public record CreateTodoCommand( - string TodoTitle, + [Required] string TodoTitle, string? TodoProject, string? TodoDescription) : IRequest { diff --git a/src/backend/server/src/Todo.Core/Application/Notifications/Todo/TodoCreated.cs b/src/backend/server/src/Todo.Core/Application/Notifications/Todo/TodoCreated.cs index 8d860ad..692adc9 100644 --- a/src/backend/server/src/Todo.Core/Application/Notifications/Todo/TodoCreated.cs +++ b/src/backend/server/src/Todo.Core/Application/Notifications/Todo/TodoCreated.cs @@ -13,10 +13,7 @@ public record TodoCreated( { private readonly ITodoPublisher _todoPublisher; - public Handler(ITodoPublisher todoPublisher) - { - _todoPublisher = todoPublisher; - } + public Handler(ITodoPublisher todoPublisher) => _todoPublisher = todoPublisher; public async Task Handle( TodoCreated notification, diff --git a/src/backend/server/src/Todo.Core/Application/Queries/Todos/GetTodoByIdQuery.cs b/src/backend/server/src/Todo.Core/Application/Queries/Todos/GetTodoByIdQuery.cs new file mode 100644 index 0000000..7653ab4 --- /dev/null +++ b/src/backend/server/src/Todo.Core/Application/Queries/Todos/GetTodoByIdQuery.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.Threading; +using MediatR; +using Todo.Core.Interfaces.Persistence; +using Todo.Core.Interfaces.User; + +namespace Todo.Core.Application.Queries.Todos; + +public record GetTodoByIdQuery([Required] string TodoId) : IRequest +{ + internal class Handler : IRequestHandler + { + private readonly ICurrentUserService _currentUserService; + private readonly ITodoRepository _todoRepository; + + public Handler(ICurrentUserService currentUserService, ITodoRepository todoRepository) + { + _currentUserService = currentUserService; + _todoRepository = todoRepository; + } + + public async Task Handle(GetTodoByIdQuery request, CancellationToken cancellationToken) + { + var userId = _currentUserService.GetUserId(); + var todo = await _todoRepository.GetTodoByIdAsync(request.TodoId); + + if (todo.AuthorId != userId) + throw new InvalidOperationException("User is not allowed to access todo"); + + return TodoViewModel.From(todo); + } + } +} \ No newline at end of file diff --git a/src/backend/server/src/Todo.Core/Application/Queries/Todos/TodoViewModel.cs b/src/backend/server/src/Todo.Core/Application/Queries/Todos/TodoViewModel.cs new file mode 100644 index 0000000..40c0761 --- /dev/null +++ b/src/backend/server/src/Todo.Core/Application/Queries/Todos/TodoViewModel.cs @@ -0,0 +1,19 @@ +namespace Todo.Core.Application.Queries.Todos; + +public record TodoViewModel( + string Id, + string Title, + bool Status, + string AuthorId, + string? Project, + string? Description +) +{ + internal static TodoViewModel From(Entities.Todo t) => new( + t.Id, + t.Title, + t.Status, + t.AuthorId, + t.Project, + t.Description); +}; \ No newline at end of file diff --git a/src/backend/server/src/Todo.Core/DependencyInjection.cs b/src/backend/server/src/Todo.Core/DependencyInjection.cs index d0c9cd0..5bf4359 100644 --- a/src/backend/server/src/Todo.Core/DependencyInjection.cs +++ b/src/backend/server/src/Todo.Core/DependencyInjection.cs @@ -11,7 +11,5 @@ namespace Todo.Core; public static class DependencyInjection { public static IServiceCollection AddCore(this IServiceCollection services) - { - return services.AddMediatR(Assembly.GetExecutingAssembly()); - } + => services.AddMediatR(Assembly.GetExecutingAssembly()); } \ No newline at end of file diff --git a/src/backend/server/src/Todo.Core/Todo.Core.csproj b/src/backend/server/src/Todo.Core/Todo.Core.csproj index e888c21..6e74587 100644 --- a/src/backend/server/src/Todo.Core/Todo.Core.csproj +++ b/src/backend/server/src/Todo.Core/Todo.Core.csproj @@ -6,16 +6,16 @@ - + - + - - + + diff --git a/src/backend/server/src/Todo.Infrastructure/DependencyInjection.cs b/src/backend/server/src/Todo.Infrastructure/DependencyInjection.cs index 8ab039f..4a55689 100644 --- a/src/backend/server/src/Todo.Infrastructure/DependencyInjection.cs +++ b/src/backend/server/src/Todo.Infrastructure/DependencyInjection.cs @@ -49,13 +49,11 @@ public static class DependencyInjection public static IApplicationBuilder UseInfrastructure( this IApplicationBuilder app) - { - return app.UseCookiePolicy( + => app.UseCookiePolicy( new CookiePolicyOptions { Secure = CookieSecurePolicy.Always }); - } } public class GiteaAuthOptions diff --git a/src/backend/server/src/Todo.Persistence/Mongo/MongoDbConnectionHandler.cs b/src/backend/server/src/Todo.Persistence/Mongo/MongoDbConnectionHandler.cs index 5063489..f5c8587 100644 --- a/src/backend/server/src/Todo.Persistence/Mongo/MongoDbConnectionHandler.cs +++ b/src/backend/server/src/Todo.Persistence/Mongo/MongoDbConnectionHandler.cs @@ -9,9 +9,7 @@ public class MongoDbConnectionHandler public MongoDbConnectionHandler( IOptionsMonitor optionsMonitor) - { - _optionsMonitor = optionsMonitor; - } + => _optionsMonitor = optionsMonitor; public IMongoDatabase CreateDatabaseConnection() { @@ -30,8 +28,5 @@ public class MongoDbConnectionHandler } 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}"; } \ No newline at end of file diff --git a/src/client/package.json b/src/client/package.json index f932373..4853736 100644 --- a/src/client/package.json +++ b/src/client/package.json @@ -10,6 +10,7 @@ "dependencies": { "@microsoft/signalr": "^6.0.0", "@tippyjs/react": "^4.2.6", + "axios": "^0.24.0", "next": "12.0.3", "next-pwa": "^5.4.0", "react": "17.0.2", diff --git a/src/client/src/boundary/todo/todoApi.ts b/src/client/src/boundary/todo/todoApi.ts new file mode 100644 index 0000000..1c6d1b7 --- /dev/null +++ b/src/client/src/boundary/todo/todoApi.ts @@ -0,0 +1,31 @@ +import axios from "axios"; +import { Todo } from "@src/core/entities/todo"; + +const getBaseUrl = () => + process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:5000"; + +const createBaseClient = () => + axios.create({ + baseURL: getBaseUrl(), + withCredentials: true, + }); + +interface CreateTodoRequest { + title: string; + description?: string; + project?: string; +} + +export const createTodoAsync = async ( + createTodo: CreateTodoRequest +): Promise => + await createBaseClient().post( + "/api/todos", + createTodo + ); + +interface GetTodoByIdResponse extends Todo {} +export const getTodoByIdAsync = async (todoId: string) => + await createBaseClient().get( + `/api/todos/${todoId}` + ); diff --git a/src/client/src/presentation/contexts/SocketContext.tsx b/src/client/src/presentation/contexts/SocketContext.tsx index 40b9797..422e772 100644 --- a/src/client/src/presentation/contexts/SocketContext.tsx +++ b/src/client/src/presentation/contexts/SocketContext.tsx @@ -5,6 +5,7 @@ import { LogLevel, } from "@microsoft/signalr"; import { asTodo, StatusState, Todo } from "@src/core/entities/todo"; +import { getTodoByIdAsync } from "@src/boundary/todo/todoApi"; interface SocketContextProps { conn: HubConnection; @@ -41,7 +42,7 @@ export const SocketProvider: FC = (props) => { const connection = new HubConnectionBuilder() .withUrl(`${serverUrl}/hubs/todo`, { - withCredentials: true + withCredentials: true, }) .withAutomaticReconnect() .configureLogging(LogLevel.Information) @@ -53,6 +54,18 @@ export const SocketProvider: FC = (props) => { setTodos(validatedTodos); }); + connection.on("todoCreated", (todoCreated) => { + const { todoId } = JSON.parse(todoCreated) as { todoId: string }; + + getTodoByIdAsync(todoId).then((response) => { + setTodos([...todos.filter((t) => t.id !== response.id), response]); + setInboxTodos([ + ...inboxTodos.filter((t) => t.id !== response.id), + response, + ]); + }); + }); + connection.on("getInboxTodos", (rawTodos) => { const newTodos = JSON.parse(rawTodos) as Todo[]; const validatedTodos = newTodos @@ -70,11 +83,14 @@ export const SocketProvider: FC = (props) => { ]); }); - connection.start().then(() => { - setConn(connection); - }).catch(e => { - window.location.href = `${serverUrl}/api/auth/login?returnUrl=${window.location.href}` - }); + connection + .start() + .then(() => { + setConn(connection); + }) + .catch((e) => { + window.location.href = `${serverUrl}/api/auth/login?returnUrl=${window.location.href}`; + }); }, []); return ( @@ -90,7 +106,9 @@ export const SocketProvider: FC = (props) => { conn.invoke("GetInboxTodos").catch(console.error); }, createTodo: (todoName, project, description) => { - conn.invoke("CreateTodo", todoName, project, description).catch(console.error); + conn + .invoke("CreateTodo", todoName, project, description) + .catch(console.error); }, updateTodo: (todoId, todoStatus) => { conn.invoke("UpdateTodo", todoId, todoStatus).catch(console.error); diff --git a/src/client/src/presentation/hooks/socketHooks.tsx b/src/client/src/presentation/hooks/socketHooks.tsx index 8271c19..3f6645f 100644 --- a/src/client/src/presentation/hooks/socketHooks.tsx +++ b/src/client/src/presentation/hooks/socketHooks.tsx @@ -1,6 +1,7 @@ import { useContext, useEffect } from "react"; import { SocketContext } from "@src/presentation/contexts/SocketContext"; import { StatusState, Todo } from "@src/core/entities/todo"; +import { createTodoAsync } from "@src/boundary/todo/todoApi"; export const useSelectInboxTodos = () => { const socketContext = useContext(SocketContext); @@ -20,7 +21,12 @@ export const useCreateTodo = () => { return { createTodo: (todoName: string, project: string, description: string) => { - socketContext.createTodo(todoName, project, description); + //socketContext.createTodo(todoName, project, description); + createTodoAsync({ + project, + description, + title: todoName, + }).catch(console.error); }, }; }; diff --git a/src/client/yarn.lock b/src/client/yarn.lock index 8e1c9fe..c1e463f 100644 --- a/src/client/yarn.lock +++ b/src/client/yarn.lock @@ -1518,6 +1518,13 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== +axios@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" + integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== + dependencies: + follow-redirects "^1.14.4" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -2812,6 +2819,11 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.4.tgz#28d9969ea90661b5134259f312ab6aa7929ac5e2" integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== +follow-redirects@^1.14.4: + version "1.14.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" + integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA== + foreach@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"