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)); => await Mediator.Send(new GetTodoByIdQuery(todoId));
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetTodos() public async Task<ActionResult<IEnumerable<TodoViewModel>>> GetTodos([FromQuery] bool onlyActive = false)
=> Ok(await _todoRepository.GetTodosAsync()); {
if (onlyActive)
return Ok(await Mediator.Send(new GetActiveTodosQuery()));
return Ok(await Mediator.Send(new GetTodosQuery()));
}
[HttpPut("{todoId}")] [HttpPut("{todoId}")]
public async Task<ActionResult> ReplaceTodo([FromRoute] string todoId, [FromBody] ReplaceTodoRequest request) public async Task<ActionResult> ReplaceTodo([FromRoute] string todoId, [FromBody] ReplaceTodoRequest request)
@ -69,11 +74,15 @@ public class TodosController : ApiController
[JsonPropertyName("status")] [JsonPropertyName("status")]
public bool Status { get; init; } public bool Status { get; init; }
[JsonPropertyName("created")]
public long Created { get; init; } = 0;
internal ReplaceTodoCommand To(string id) => new( internal ReplaceTodoCommand To(string id) => new(
id, id,
Title, Title,
Project, Project,
Description, Description,
Status); Status,
Created);
} }
} }

View File

@ -14,20 +14,14 @@ namespace Todo.Api.Hubs;
public class TodoHub : Hub public class TodoHub : Hub
{ {
private readonly ICurrentUserService _currentUserService; private readonly ICurrentUserService _currentUserService;
private readonly IMediator _mediator;
private readonly ITodoRepository _todoRepository;
private readonly IUserConnectionStore _userConnectionStore; private readonly IUserConnectionStore _userConnectionStore;
public TodoHub( public TodoHub(
ITodoRepository todoRepository,
IUserConnectionStore userConnectionStore, IUserConnectionStore userConnectionStore,
ICurrentUserService currentUserService, ICurrentUserService currentUserService)
IMediator mediator)
{ {
_todoRepository = todoRepository;
_userConnectionStore = userConnectionStore; _userConnectionStore = userConnectionStore;
_currentUserService = currentUserService; _currentUserService = currentUserService;
_mediator = mediator;
} }
public override Task OnConnectedAsync() public override Task OnConnectedAsync()
@ -46,149 +40,4 @@ public class TodoHub : Hub
return base.OnDisconnectedAsync(exception); 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( public record ReplaceTodoCommand(
string Id, string Id,
string Title, string Title,
string Project, string? Project,
string Description, string? Description,
bool Status) : IRequest<Unit> bool Status,
long Created) : IRequest<Unit>
{ {
internal class Handler : IRequestHandler<ReplaceTodoCommand, Unit> internal class Handler : IRequestHandler<ReplaceTodoCommand, Unit>
{ {
@ -43,13 +44,12 @@ public record ReplaceTodoCommand(
} }
} }
private Entities.Todo To(string authorId) => new() private Entities.Todo To(string authorId) => new(
{ Id,
Id = Id, Title,
Description = Description, Status,
Project = Project, Project,
Status = Status, authorId,
Title = Title, Description,
AuthorId = authorId 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; namespace Todo.Core.Entities;
public record Todo public record Todo(
{ string Id,
public string Id { get; init; } string Title,
public string Title { get; init; } bool Status,
public bool Status { get; init; } string? Project,
public string? Project { get; init; } string AuthorId,
public string AuthorId { get; init; } string? Description,
public string? Description { get; init; } long Created);
public long Created { get; init; }
}

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace Todo.Core.Interfaces.Persistence; namespace Todo.Core.Interfaces.Persistence;
public interface ITodoRepository public interface ITodoRepository
@ -9,14 +11,14 @@ public interface ITodoRepository
string userId, string userId,
long created); long created);
Task<IEnumerable<Entities.Todo>> GetTodosAsync(); Task<IEnumerable<Entities.Todo>> GetTodosAsync(string userId, CancellationToken cancellationToken = new());
Task UpdateTodoStatus( Task UpdateTodoStatus(
string todoId, string todoId,
bool todoStatus, bool todoStatus,
string userId); string userId);
Task<IEnumerable<Entities.Todo>> GetNotDoneTodos();
Task<Entities.Todo> GetTodoByIdAsync(string todoId); Task<Entities.Todo> GetTodoByIdAsync(string todoId);
Task<Entities.Todo> UpdateTodoAsync(Entities.Todo todo); Task<Entities.Todo> UpdateTodoAsync(Entities.Todo todo);
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? ProjectName { get; set; } = string.Empty;
public string AuthorId { get; set; } public string AuthorId { get; set; }
public long Created { get; set; } = 0; 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.Diagnostics;
using System.Threading;
using MongoDB.Driver; using MongoDB.Driver;
using Todo.Core.Interfaces.Persistence; using Todo.Core.Interfaces.Persistence;
using Todo.Persistence.Mongo.Repositories.Dtos; using Todo.Persistence.Mongo.Repositories.Dtos;
@ -27,40 +28,21 @@ public class TodoRepository : ITodoRepository
{ {
Title = title, Title = title,
ProjectName = projectName, ProjectName = projectName,
Status = false,
AuthorId = userId, AuthorId = userId,
Description = description, Description = description,
Created = created Created = created
}; };
await _todosCollection.InsertOneAsync(todo); await _todosCollection.InsertOneAsync(todo);
return new Core.Entities.Todo return todo.To();
{
Id = todo.Id,
Title = todo.Title,
Status = false,
Project = todo.ProjectName,
Description = todo.Description,
AuthorId = todo.AuthorId,
Created = todo.Created
};
} }
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 return todos
.ToEnumerable() .ToEnumerable()
.Select( .Select(t => t.To());
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
});
} }
public async Task UpdateTodoStatus( public async Task UpdateTodoStatus(
@ -74,27 +56,12 @@ public class TodoRepository : ITodoRepository
Builders<MongoTodo>.Update.Set(t => t.Status, todoStatus)); 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) public async Task<Core.Entities.Todo> GetTodoByIdAsync(string todoId)
{ {
var todoCursor = await _todosCollection.FindAsync(f => f.Id == todoId); var todoCursor = await _todosCollection.FindAsync(f => f.Id == todoId);
var todo = await todoCursor.FirstOrDefaultAsync(); var todo = await todoCursor.FirstOrDefaultAsync();
return new Core.Entities.Todo return todo.To();
{
Id = todo.Id,
Project = todo.ProjectName,
Status = todo.Status,
Title = todo.Title,
Description = todo.Description,
AuthorId = todo.AuthorId,
Created = todo.Created
};
} }
public async Task<Core.Entities.Todo> UpdateTodoAsync( public async Task<Core.Entities.Todo> UpdateTodoAsync(
@ -113,15 +80,18 @@ public class TodoRepository : ITodoRepository
Created = todo.Created Created = todo.Created
}); });
return new Core.Entities.Todo return updatedTodo.To();
{ }
Id = updatedTodo.Id,
Project = updatedTodo.ProjectName, public async Task<IEnumerable<Core.Entities.Todo>> GetActiveTodosAsync(
Status = updatedTodo.Status, string authorId,
Title = updatedTodo.Title, CancellationToken cancellationToken)
AuthorId = updatedTodo.AuthorId, {
Description = updatedTodo.Description, var todosCursor = await _todosCollection.FindAsync(
Created = updatedTodo.Created 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>>({ createTodo: build.mutation<string, Partial<Todo>>({
query(body) { query(body) {
return { return {

View File

@ -6,7 +6,7 @@ import { todosApi } from "@src/infrastructure/apis/todosApi";
import { AddTodo } from "@src/components/todos/addTodo"; import { AddTodo } from "@src/components/todos/addTodo";
const HomePage = () => { const HomePage = () => {
const { data, isLoading } = todosApi.useGetAllTodosQuery(); const { data, isLoading } = todosApi.useGetActiveTodosQuery();
if (isLoading) { if (isLoading) {
return "loading..."; return "loading...";