Updated with more secure modelling on backend

This commit is contained in:
Kasper Juul Hermansen 2021-11-20 01:28:53 +01:00
parent 4f3b19891a
commit bb99f99a22
Signed by: kjuulh
GPG Key ID: DCD9397082D97069
11 changed files with 151 additions and 230 deletions

View File

@ -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);
}
}

View File

@ -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");
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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());
}

View File

@ -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);
}

View File

@ -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
{
Id = updatedTodo.Id,
Project = updatedTodo.ProjectName,
Status = updatedTodo.Status,
Title = updatedTodo.Title,
AuthorId = updatedTodo.AuthorId,
Description = updatedTodo.Description,
Created = updatedTodo.Created
};
return updatedTodo.To();
}
public async Task<IEnumerable<Core.Entities.Todo>> GetActiveTodosAsync(
string authorId,
CancellationToken cancellationToken)
{
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());
}
}

View File

@ -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 {

View File

@ -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...";