Skip to content

Commit

Permalink
merge(#33): add reactions
Browse files Browse the repository at this point in the history
  • Loading branch information
undrcrxwn committed Oct 14, 2024
2 parents 30c010c + 157cc21 commit cee9b16
Show file tree
Hide file tree
Showing 40 changed files with 872 additions and 498 deletions.
2 changes: 1 addition & 1 deletion src/CrowdParlay.Social.Api/AuthenticationConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ namespace CrowdParlay.Social.Api;
public static class AuthenticationConstants
{
public const string CookieAuthenticationUserIdClaim = "user_id";
public const string BearerAuthenticationUserIdClaim = "sub";
public const string JwtBearerAuthenticationUserIdClaim = "sub";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Security.Claims;
using CrowdParlay.Social.Application.Exceptions;

namespace CrowdParlay.Social.Api.Extensions;

Expand All @@ -8,8 +9,11 @@ public static class ClaimsPrincipalExtensions
{
var userIdClaim = principal.Claims.FirstOrDefault(claim => claim.Type
is AuthenticationConstants.CookieAuthenticationUserIdClaim
or AuthenticationConstants.BearerAuthenticationUserIdClaim);
or AuthenticationConstants.JwtBearerAuthenticationUserIdClaim);

return Guid.TryParse(userIdClaim?.Value, out var value) ? value : null;
}

public static Guid GetRequiredUserId(this ClaimsPrincipal principal) =>
principal.GetUserId() ?? throw new ForbiddenException();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static IServiceCollection ConfigureAuthentication(this IServiceCollection

builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.MapInboundClaims = false;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters.ValidateAudience = false;
options.TokenValidationParameters.ValidateIssuer = false;
Expand Down
4 changes: 2 additions & 2 deletions src/CrowdParlay.Social.Api/GlobalSerializerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public static class GlobalSerializerOptions
{
public static readonly JsonSerializerOptions SnakeCase = new()
{
PropertyNamingPolicy = new SnakeCaseJsonNamingPolicy(),
DictionaryKeyPolicy = new SnakeCaseJsonNamingPolicy()
PropertyNamingPolicy = new SnakeCaseJsonNamingPolicy(),
DictionaryKeyPolicy = new SnakeCaseJsonNamingPolicy()
};
}
53 changes: 46 additions & 7 deletions src/CrowdParlay.Social.Api/v1/Controllers/CommentsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using CrowdParlay.Social.Application.DTOs;
using CrowdParlay.Social.Application.Exceptions;
using CrowdParlay.Social.Domain.DTOs;
using CrowdParlay.Social.Domain.ValueObjects;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
Expand All @@ -26,7 +27,7 @@ public class CommentsController(ICommentsService comments, IHubContext<CommentsH
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.InternalServerError)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.NotFound)]
public async Task<CommentDto> GetCommentById([FromRoute] Guid commentId) =>
await comments.GetByIdAsync(commentId);
await comments.GetByIdAsync(commentId, User.GetUserId());

/// <summary>
/// Get comments by filters.
Expand All @@ -41,7 +42,7 @@ public async Task<Page<CommentDto>> SearchComments(
[FromQuery] Guid? authorId,
[FromQuery, BindRequired] int offset,
[FromQuery, BindRequired] int count) =>
await comments.SearchAsync(discussionId, authorId, offset, count);
await comments.SearchAsync(discussionId, authorId, User.GetUserId(), offset, count);

/// <summary>
/// Creates a top-level comment in discussion.
Expand Down Expand Up @@ -80,7 +81,7 @@ public async Task<Page<CommentDto>> GetRepliesToComment(
[FromRoute] Guid parentCommentId,
[FromQuery, BindRequired] int offset,
[FromQuery, BindRequired] int count) =>
await comments.GetRepliesToCommentAsync(parentCommentId, offset, count);
await comments.GetRepliesToCommentAsync(parentCommentId, User.GetUserId(), offset, count);

/// <summary>
/// Creates a reply to the comment with the specified ID.
Expand All @@ -94,11 +95,49 @@ public async Task<Page<CommentDto>> GetRepliesToComment(
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.NotFound)]
public async Task<ActionResult<CommentDto>> ReplyToComment([FromRoute] Guid parentCommentId, [FromBody] ReplyRequest request)
{
var authorId =
User.GetUserId()
?? throw new ForbiddenException();
var response = await comments.ReplyToCommentAsync(User.GetRequiredUserId(), parentCommentId, request.Content);
return CreatedAtAction(nameof(GetCommentById), new { commentId = response.Id }, response);
}

var response = await comments.ReplyToCommentAsync(authorId, parentCommentId, request.Content);
/// <summary>
/// React to the comment.
/// </summary>
[HttpPost("{commentId:guid}/replies"), Authorize]
[Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(CommentDto), (int)HttpStatusCode.Created)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.InternalServerError)]
[ProducesResponseType(typeof(ValidationProblemDetails), (int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.Forbidden)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.NotFound)]
public async Task<ActionResult<CommentDto>> ReplyToComment([FromRoute] Guid commentId, [FromBody] string reaction)
{
var response = await comments.AddReactionAsync(User.GetRequiredUserId(), commentId, new Reaction(reaction));
return CreatedAtAction(nameof(GetCommentById), new { commentId = response.Id }, response);
}

/// <summary>
/// Add a reaction to a comment
/// </summary>
[HttpPost("{commentId:guid}/reactions"), Authorize]
[Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(CommentDto), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.InternalServerError)]
[ProducesResponseType(typeof(ValidationProblemDetails), (int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.Forbidden)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.NotFound)]
public async Task<CommentDto> AddReaction([FromRoute] Guid commentId, [FromBody] string reaction) =>
await comments.AddReactionAsync(User.GetRequiredUserId(), commentId, reaction);

/// <summary>
/// Remove a reaction from a comment
/// </summary>
[HttpDelete("{commentId:guid}/reactions"), Authorize]
[Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(CommentDto), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.InternalServerError)]
[ProducesResponseType(typeof(ValidationProblemDetails), (int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.Forbidden)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.NotFound)]
public async Task<CommentDto> RemoveReaction([FromRoute] Guid commentId, [FromBody] string reaction) =>
await comments.AddReactionAsync(User.GetRequiredUserId(), commentId, reaction);
}
44 changes: 32 additions & 12 deletions src/CrowdParlay.Social.Api/v1/Controllers/DiscussionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using CrowdParlay.Social.Api.v1.DTOs;
using CrowdParlay.Social.Application.Abstractions;
using CrowdParlay.Social.Application.DTOs;
using CrowdParlay.Social.Application.Exceptions;
using CrowdParlay.Social.Domain.DTOs;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -13,7 +12,7 @@
namespace CrowdParlay.Social.Api.v1.Controllers;

[ApiController, ApiRoute("[controller]")]
public class DiscussionsController(IDiscussionsService discussionsService) : ControllerBase
public class DiscussionsController(IDiscussionsService discussions) : ControllerBase
{
/// <summary>
/// Returns discussion with the specified ID.
Expand All @@ -23,20 +22,19 @@ public class DiscussionsController(IDiscussionsService discussionsService) : Con
[ProducesResponseType(typeof(DiscussionDto), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.NotFound)]
public async Task<DiscussionDto> GetDiscussionById([FromRoute] Guid discussionId) =>
await discussionsService.GetByIdAsync(discussionId);
await discussions.GetByIdAsync(discussionId, User.GetUserId());

/// <summary>
/// Returns all discussions created by author with the specified ID.
/// </summary>
[HttpGet]
[Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(Page<DiscussionDto>), (int)HttpStatusCode.OK)]
public async Task<Page<DiscussionDto>> GetDiscussions(
public async Task<Page<DiscussionDto>> SearchDiscussions(
[FromQuery] Guid? authorId,
[FromQuery, BindRequired] int offset,
[FromQuery, BindRequired] int count) => authorId is null
? await discussionsService.GetAllAsync(offset, count)
: await discussionsService.GetByAuthorAsync(authorId.Value, offset, count);
[FromQuery, BindRequired] int count) =>
await discussions.SearchAsync(authorId, User.GetUserId(), offset, count);

/// <summary>
/// Creates a discussion.
Expand All @@ -47,11 +45,33 @@ public async Task<Page<DiscussionDto>> GetDiscussions(
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.Forbidden)]
public async Task<ActionResult<DiscussionDto>> CreateDiscussion([FromBody] DiscussionRequest request)
{
var authorId =
User.GetUserId()
?? throw new ForbiddenException();

var response = await discussionsService.CreateAsync(authorId, request.Title, request.Description);
var response = await discussions.CreateAsync(User.GetRequiredUserId(), request.Title, request.Description);
return CreatedAtAction(nameof(GetDiscussionById), new { DiscussionId = response.Id }, response);
}

/// <summary>
/// Add a reaction to a comment
/// </summary>
[HttpPost("{discussionId:guid}/reactions"), Authorize]
[Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(DiscussionDto), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.InternalServerError)]
[ProducesResponseType(typeof(ValidationProblemDetails), (int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.Forbidden)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.NotFound)]
public async Task<DiscussionDto> AddReaction([FromRoute] Guid discussionId, [FromBody] string reaction) =>
await discussions.AddReactionAsync(User.GetRequiredUserId(), discussionId, reaction);

/// <summary>
/// Remove a reaction from a comment
/// </summary>
[HttpDelete("{commentId:guid}/reactions"), Authorize]
[Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(DiscussionDto), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.InternalServerError)]
[ProducesResponseType(typeof(ValidationProblemDetails), (int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.Forbidden)]
[ProducesResponseType(typeof(ProblemDetails), (int)HttpStatusCode.NotFound)]
public async Task<DiscussionDto> RemoveReaction([FromRoute] Guid discussionId, [FromBody] string reaction) =>
await discussions.AddReactionAsync(User.GetRequiredUserId(), discussionId, reaction);
}
13 changes: 13 additions & 0 deletions src/CrowdParlay.Social.Api/v1/Controllers/LookupController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Net.Mime;
using CrowdParlay.Social.Domain.ValueObjects;
using Microsoft.AspNetCore.Mvc;

namespace CrowdParlay.Social.Api.v1.Controllers;

[ApiController, ApiRoute("[controller]")]
public class LookupController : ControllerBase
{
[HttpGet("reactions")]
[Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)]
public IReadOnlySet<string> GetAvailableReactions() => Reaction.AllowedValues;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
using CrowdParlay.Social.Application.DTOs;
using CrowdParlay.Social.Domain.DTOs;
using CrowdParlay.Social.Domain.ValueObjects;

namespace CrowdParlay.Social.Application.Abstractions;

public interface ICommentsService
{
public Task<CommentDto> GetByIdAsync(Guid id);
public Task<Page<CommentDto>> SearchAsync(Guid? discussionId, Guid? authorId, int offset, int count);
public Task<CommentDto> GetByIdAsync(Guid commentId, Guid? viewerId);
public Task<Page<CommentDto>> SearchAsync(Guid? discussionId, Guid? authorId, Guid? viewerId, int offset, int count);
public Task<CommentDto> CreateAsync(Guid authorId, Guid discussionId, string content);
public Task<Page<CommentDto>> GetRepliesToCommentAsync(Guid parentCommentId, int offset, int count);
public Task<Page<CommentDto>> GetRepliesToCommentAsync(Guid parentCommentId, Guid? viewerId, int offset, int count);
public Task<CommentDto> ReplyToCommentAsync(Guid authorId, Guid parentCommentId, string content);
public Task<CommentDto> AddReactionAsync(Guid authorId, Guid commentId, Reaction reaction);
public Task<CommentDto> RemoveReactionAsync(Guid authorId, Guid commentId, Reaction reaction);
public Task DeleteAsync(Guid id);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using CrowdParlay.Social.Application.DTOs;
using CrowdParlay.Social.Domain.DTOs;
using CrowdParlay.Social.Domain.ValueObjects;

namespace CrowdParlay.Social.Application.Abstractions;

public interface IDiscussionsService
{
public Task<DiscussionDto> GetByIdAsync(Guid id);
public Task<Page<DiscussionDto>> GetAllAsync(int offset, int count);
public Task<Page<DiscussionDto>> GetByAuthorAsync(Guid authorId, int offset, int count);
public Task<DiscussionDto> GetByIdAsync(Guid discussionId, Guid? viewerId);
public Task<Page<DiscussionDto>> SearchAsync(Guid? authorId, Guid? viewerId, int offset, int count);
public Task<DiscussionDto> CreateAsync(Guid authorId, string title, string description);
public Task<DiscussionDto> AddReactionAsync(Guid authorId, Guid discussionId, Reaction reaction);
public Task<DiscussionDto> RemoveReactionAsync(Guid authorId, Guid discussionId, Reaction reaction);
}
2 changes: 2 additions & 0 deletions src/CrowdParlay.Social.Application/DTOs/CommentDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ public class CommentDto
public required DateTimeOffset CreatedAt { get; set; }
public required int ReplyCount { get; set; }
public required IEnumerable<AuthorDto> FirstRepliesAuthors { get; set; }
public required IDictionary<string, int> ReactionCounters { get; set; }
public required ISet<string> ViewerReactions { get; set; }
}
2 changes: 2 additions & 0 deletions src/CrowdParlay.Social.Application/DTOs/DiscussionDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ public class DiscussionDto
public required string Description { get; set; }
public required AuthorDto? Author { get; set; }
public required DateTimeOffset CreatedAt { get; set; }
public required IDictionary<string, int> ReactionCounters { get; set; }
public required ISet<string> ViewerReactions { get; set; }
}
Loading

0 comments on commit cee9b16

Please sign in to comment.