Updated with more secure modelling on backend
This commit is contained in:
parent
4f3b19891a
commit
bb99f99a22
@ -44,8 +44,13 @@ public class TodosController : ApiController
|
||||
=> await Mediator.Send(new GetTodoByIdQuery(todoId));
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetTodos()
|
||||
=> Ok(await _todoRepository.GetTodosAsync());
|
||||
public async Task<ActionResult<IEnumerable<TodoViewModel>>> GetTodos([FromQuery] bool onlyActive = false)
|
||||
{
|
||||
if (onlyActive)
|
||||
return Ok(await Mediator.Send(new GetActiveTodosQuery()));
|
||||
|
||||
return Ok(await Mediator.Send(new GetTodosQuery()));
|
||||
}
|
||||
|
||||
[HttpPut("{todoId}")]
|
||||
public async Task<ActionResult> ReplaceTodo([FromRoute] string todoId, [FromBody] ReplaceTodoRequest request)
|
||||
@ -69,11 +74,15 @@ public class TodosController : ApiController
|
||||
[JsonPropertyName("status")]
|
||||
public bool Status { get; init; }
|
||||
|
||||
[JsonPropertyName("created")]
|
||||
public long Created { get; init; } = 0;
|
||||
|
||||
internal ReplaceTodoCommand To(string id) => new(
|
||||
id,
|
||||
Title,
|
||||
Project,
|
||||
Description,
|
||||
Status);
|
||||
Status,
|
||||
Created);
|
||||
}
|
||||
}
|
@ -14,20 +14,14 @@ namespace Todo.Api.Hubs;
|
||||
public class TodoHub : Hub
|
||||
{
|
||||
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)
|
||||
ICurrentUserService currentUserService)
|
||||
{
|
||||
_todoRepository = todoRepository;
|
||||
_userConnectionStore = userConnectionStore;
|
||||
_currentUserService = currentUserService;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public override Task OnConnectedAsync()
|
||||
@ -46,149 +40,4 @@ public class TodoHub : Hub
|
||||
|
||||
return base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
|
||||
public async Task CreateTodo(
|
||||
string todoTitle,
|
||||
string? projectName,
|
||||
string? description)
|
||||
{
|
||||
if (todoTitle is null)
|
||||
throw new ArgumentException("title cannot be null");
|
||||
|
||||
//var userId = GetUserId();
|
||||
|
||||
//var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName, userId);
|
||||
await _mediator.Send(
|
||||
new CreateTodoCommand(
|
||||
todoTitle,
|
||||
projectName,
|
||||
description));
|
||||
|
||||
await GetInboxTodos();
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateTodo(string todoId, bool todoStatus)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
await _todoRepository.UpdateTodoStatus(
|
||||
todoId,
|
||||
todoStatus,
|
||||
userId);
|
||||
|
||||
await GetInboxTodos();
|
||||
}
|
||||
|
||||
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,
|
||||
Description = t.Description
|
||||
})
|
||||
.ToList());
|
||||
|
||||
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,
|
||||
Description = t.Description
|
||||
})
|
||||
.ToList());
|
||||
|
||||
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,
|
||||
Description = todo.Description
|
||||
});
|
||||
|
||||
await RunOnUserConnections(
|
||||
async connections =>
|
||||
await Clients.Clients(connections)
|
||||
.SendAsync("getTodo", serializedTodo));
|
||||
}
|
||||
|
||||
public async Task ReplaceTodo(string 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,
|
||||
Description = updateTodo.Description
|
||||
});
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private async Task RunOnUserConnections(
|
||||
Func<IEnumerable<string>, Task> action)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
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");
|
||||
}
|
@ -9,9 +9,10 @@ namespace Todo.Core.Application.Commands.Todo;
|
||||
public record ReplaceTodoCommand(
|
||||
string Id,
|
||||
string Title,
|
||||
string Project,
|
||||
string Description,
|
||||
bool Status) : IRequest<Unit>
|
||||
string? Project,
|
||||
string? Description,
|
||||
bool Status,
|
||||
long Created) : IRequest<Unit>
|
||||
{
|
||||
internal class Handler : IRequestHandler<ReplaceTodoCommand, Unit>
|
||||
{
|
||||
@ -43,13 +44,12 @@ public record ReplaceTodoCommand(
|
||||
}
|
||||
}
|
||||
|
||||
private Entities.Todo To(string authorId) => new()
|
||||
{
|
||||
Id = Id,
|
||||
Description = Description,
|
||||
Project = Project,
|
||||
Status = Status,
|
||||
Title = Title,
|
||||
AuthorId = authorId
|
||||
};
|
||||
private Entities.Todo To(string authorId) => new(
|
||||
Id,
|
||||
Title,
|
||||
Status,
|
||||
Project,
|
||||
authorId,
|
||||
Description,
|
||||
Created);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using System.Threading;
|
||||
using MediatR;
|
||||
using Todo.Core.Interfaces.Persistence;
|
||||
using Todo.Core.Interfaces.User;
|
||||
|
||||
namespace Todo.Core.Application.Queries.Todos;
|
||||
|
||||
public record GetActiveTodosQuery : IRequest<IEnumerable<TodoViewModel>>
|
||||
{
|
||||
internal class Handler : IRequestHandler<GetActiveTodosQuery, IEnumerable<TodoViewModel>>
|
||||
{
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly ITodoRepository _todoRepository;
|
||||
|
||||
public Handler(ICurrentUserService currentUserService, ITodoRepository todoRepository)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
_todoRepository = todoRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TodoViewModel>> Handle(
|
||||
GetActiveTodosQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = _currentUserService.GetUserId();
|
||||
if (userId is null)
|
||||
throw new InvalidOperationException("Cannot get userId");
|
||||
|
||||
var todos = await _todoRepository.GetActiveTodosAsync(userId);
|
||||
|
||||
return todos.Select(TodoViewModel.From);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using System.Threading;
|
||||
using MediatR;
|
||||
using Todo.Core.Interfaces.Persistence;
|
||||
using Todo.Core.Interfaces.User;
|
||||
|
||||
namespace Todo.Core.Application.Queries.Todos;
|
||||
|
||||
public record GetTodosQuery : IRequest<IEnumerable<TodoViewModel>>
|
||||
{
|
||||
internal class Handler : IRequestHandler<GetTodosQuery, IEnumerable<TodoViewModel>>
|
||||
{
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly ITodoRepository _todoRepository;
|
||||
|
||||
public Handler(ICurrentUserService currentUserService, ITodoRepository todoRepository)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
_todoRepository = todoRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TodoViewModel>> Handle(
|
||||
GetTodosQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = _currentUserService.GetUserId();
|
||||
if (userId is null)
|
||||
throw new InvalidOperationException("Cannot get userId");
|
||||
|
||||
var todos = await _todoRepository.GetTodosAsync(userId);
|
||||
|
||||
return todos.Select(TodoViewModel.From);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
namespace Todo.Core.Entities;
|
||||
|
||||
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 AuthorId { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public long Created { get; init; }
|
||||
}
|
||||
public record Todo(
|
||||
string Id,
|
||||
string Title,
|
||||
bool Status,
|
||||
string? Project,
|
||||
string AuthorId,
|
||||
string? Description,
|
||||
long Created);
|
@ -1,3 +1,5 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace Todo.Core.Interfaces.Persistence;
|
||||
|
||||
public interface ITodoRepository
|
||||
@ -9,14 +11,14 @@ public interface ITodoRepository
|
||||
string userId,
|
||||
long created);
|
||||
|
||||
Task<IEnumerable<Entities.Todo>> GetTodosAsync();
|
||||
Task<IEnumerable<Entities.Todo>> GetTodosAsync(string userId, CancellationToken cancellationToken = new());
|
||||
|
||||
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);
|
||||
Task<IEnumerable<Entities.Todo>> GetActiveTodosAsync(string authorId, CancellationToken cancellationToken = new());
|
||||
}
|
@ -20,4 +20,13 @@ public record MongoTodo
|
||||
public string? ProjectName { get; set; } = string.Empty;
|
||||
public string AuthorId { get; set; }
|
||||
public long Created { get; set; } = 0;
|
||||
|
||||
public Core.Entities.Todo To() => new(
|
||||
Id,
|
||||
Title,
|
||||
Status,
|
||||
ProjectName,
|
||||
AuthorId,
|
||||
Description,
|
||||
Created);
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using MongoDB.Driver;
|
||||
using Todo.Core.Interfaces.Persistence;
|
||||
using Todo.Persistence.Mongo.Repositories.Dtos;
|
||||
@ -27,40 +28,21 @@ public class TodoRepository : ITodoRepository
|
||||
{
|
||||
Title = title,
|
||||
ProjectName = projectName,
|
||||
Status = false,
|
||||
AuthorId = userId,
|
||||
Description = description,
|
||||
Created = created
|
||||
};
|
||||
await _todosCollection.InsertOneAsync(todo);
|
||||
return new Core.Entities.Todo
|
||||
{
|
||||
Id = todo.Id,
|
||||
Title = todo.Title,
|
||||
Status = false,
|
||||
Project = todo.ProjectName,
|
||||
Description = todo.Description,
|
||||
AuthorId = todo.AuthorId,
|
||||
Created = todo.Created
|
||||
};
|
||||
return todo.To();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.Todo>> GetTodosAsync()
|
||||
public async Task<IEnumerable<Core.Entities.Todo>> GetTodosAsync(string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var todos = await _todosCollection.FindAsync(_ => true);
|
||||
var todos = await _todosCollection.FindAsync(t => t.AuthorId == userId, cancellationToken: cancellationToken);
|
||||
return todos
|
||||
.ToEnumerable()
|
||||
.Select(
|
||||
t =>
|
||||
new Core.Entities.Todo
|
||||
{
|
||||
Id = t.Id,
|
||||
Title = t.Title,
|
||||
Status = t.Status,
|
||||
Project = t.ProjectName,
|
||||
Description = t.Description,
|
||||
AuthorId = t.AuthorId,
|
||||
Created = t.Created
|
||||
});
|
||||
.Select(t => t.To());
|
||||
}
|
||||
|
||||
public async Task UpdateTodoStatus(
|
||||
@ -74,27 +56,12 @@ public class TodoRepository : ITodoRepository
|
||||
Builders<MongoTodo>.Update.Set(t => t.Status, todoStatus));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.Todo>> GetNotDoneTodos()
|
||||
{
|
||||
var todos = await GetTodosAsync();
|
||||
return todos.Where(t => t.Status == false);
|
||||
}
|
||||
|
||||
public async Task<Core.Entities.Todo> GetTodoByIdAsync(string todoId)
|
||||
{
|
||||
var todoCursor = await _todosCollection.FindAsync(f => f.Id == todoId);
|
||||
var todo = await todoCursor.FirstOrDefaultAsync();
|
||||
|
||||
return new Core.Entities.Todo
|
||||
{
|
||||
Id = todo.Id,
|
||||
Project = todo.ProjectName,
|
||||
Status = todo.Status,
|
||||
Title = todo.Title,
|
||||
Description = todo.Description,
|
||||
AuthorId = todo.AuthorId,
|
||||
Created = todo.Created
|
||||
};
|
||||
return todo.To();
|
||||
}
|
||||
|
||||
public async Task<Core.Entities.Todo> UpdateTodoAsync(
|
||||
@ -113,15 +80,18 @@ public class TodoRepository : ITodoRepository
|
||||
Created = todo.Created
|
||||
});
|
||||
|
||||
return new Core.Entities.Todo
|
||||
return updatedTodo.To();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.Todo>> GetActiveTodosAsync(
|
||||
string authorId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Id = updatedTodo.Id,
|
||||
Project = updatedTodo.ProjectName,
|
||||
Status = updatedTodo.Status,
|
||||
Title = updatedTodo.Title,
|
||||
AuthorId = updatedTodo.AuthorId,
|
||||
Description = updatedTodo.Description,
|
||||
Created = updatedTodo.Created
|
||||
};
|
||||
var todosCursor = await _todosCollection.FindAsync(
|
||||
t => t.AuthorId == authorId && t.Status == false,
|
||||
cancellationToken: cancellationToken);
|
||||
var todos = todosCursor.ToEnumerable(cancellationToken);
|
||||
|
||||
return todos.Select(t => t.To());
|
||||
}
|
||||
}
|
@ -23,6 +23,22 @@ export const todosApi = createApi({
|
||||
: [],
|
||||
}),
|
||||
|
||||
getActiveTodos: build.query<Todo[], void>({
|
||||
query: () => ({
|
||||
url: `api/todos`,
|
||||
params: {
|
||||
onlyActive: true,
|
||||
},
|
||||
}),
|
||||
providesTags: (result) =>
|
||||
result
|
||||
? [
|
||||
...result.map(({ id }) => ({ type: "todo" as const, id })),
|
||||
{ type: "todo", id: "LIST" },
|
||||
]
|
||||
: [],
|
||||
}),
|
||||
|
||||
createTodo: build.mutation<string, Partial<Todo>>({
|
||||
query(body) {
|
||||
return {
|
||||
|
@ -6,7 +6,7 @@ import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||
import { AddTodo } from "@src/components/todos/addTodo";
|
||||
|
||||
const HomePage = () => {
|
||||
const { data, isLoading } = todosApi.useGetAllTodosQuery();
|
||||
const { data, isLoading } = todosApi.useGetActiveTodosQuery();
|
||||
|
||||
if (isLoading) {
|
||||
return "loading...";
|
||||
|
Loading…
Reference in New Issue
Block a user