Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .idea/.idea.Messager/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions API/API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
<PackageReference Include="MediatR" Version="12.*" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion API/Contracts/AuthContracts.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace API.Contracts;
namespace Api.Contracts;

public sealed record RegisterRequest(string DerBase64, string UserName, uint UserTag);
public sealed record RegisterResponse(string FingerprintSha512, string UserName, uint UserTag);
Expand Down
2 changes: 1 addition & 1 deletion API/Contracts/KeyExchangeContracts.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace API.Contracts;
namespace Api.Contracts;

public sealed record SendKeyExchangeRequest(string ToPublicKey, string EncryptedPrivateKeyBase64);
public sealed record KeyExchangeResponse(string FromPublicKey, string ToPublicKey, string EncryptedPrivateKeyBase64, DateTime CreatedAt);
2 changes: 1 addition & 1 deletion API/Contracts/MessageContracts.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace API.Contracts;
namespace Api.Contracts;

public sealed record SendMessageRequest(string ToPublicKey, string EncryptedContentBase64, string MessageHash);
public sealed record MessageResponse(string FromPublicKey, string ToPublicKey, string EncryptedContentBase64, string MessageHash, DateTime CreatedAt);
2 changes: 1 addition & 1 deletion API/Contracts/PublicKeyContracts.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
namespace API.Contracts;
namespace Api.Contracts;

public sealed record PublicKeyProfileResponse(string FingerprintSha512, string UserName, uint UserTag, string PublicKeyDerBase64);
2 changes: 1 addition & 1 deletion API/Contracts/SyncContracts.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace API.Contracts;
namespace Api.Contracts;

public sealed record SyncDeltaResponse(
DateTime ServerTimeUtc,
Expand Down
63 changes: 63 additions & 0 deletions API/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Api.Contracts;
using Api.Security;
using Application.Commands;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;

namespace Api.Controllers;

[ApiController]
[Route("/api/auth")]
[EnableRateLimiting("auth")]
public sealed class AuthController(IMediator mediator, JwtTokenIssuer jwtTokenIssuer) : ControllerBase
{
[HttpPost("register")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status201Created, Type = typeof(RegisterResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ErrorResponse))]
public async Task<IActionResult> RegisterAsync(
[FromBody] RegisterRequest request,
CancellationToken cancellationToken)
{
RegisterResult result = await mediator.Send(
new RegisterCommand(request.DerBase64, request.UserName, request.UserTag),
cancellationToken);

return Created(
$"/api/public-keys/{result.FingerprintSha512}",
new RegisterResponse(result.FingerprintSha512, result.UserName, result.UserTag));
}

[HttpPost("challenge")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ChallengeResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
public async Task<IActionResult> ChallengeAsync(
[FromBody] ChallengeRequest request,
CancellationToken cancellationToken)
{
byte[] challenge = await mediator.Send(new GetLoginChallengeCommand(request.FingerprintSha512), cancellationToken);
return Ok(new ChallengeResponse(Convert.ToBase64String(challenge)));
}

[HttpPost("login")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(LoginResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
public async Task<IActionResult> LoginAsync(
[FromBody] LoginRequest request,
CancellationToken cancellationToken)
{
await mediator.Send(
new LoginCommand(request.FingerprintSha512, request.ChallengeBase64, request.SignatureBase64),
cancellationToken);

(string token, DateTime expiresAtUtc) = jwtTokenIssuer.Generate(request.FingerprintSha512);
return Ok(new LoginResponse(token, expiresAtUtc));
}
}
52 changes: 52 additions & 0 deletions API/Controllers/KeyExchangesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Api.Contracts;
using Api.Extensions;
using Application.Commands;
using Application.DTOs;
using Application.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Api.Controllers;

[ApiController]
[Authorize]
[Route("/api/key-exchanges")]
public sealed class KeyExchangesController(IMediator mediator) : ControllerBase
{
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(KeyExchangeResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SendKeyExchangeAsync(
[FromBody] SendKeyExchangeRequest request,
CancellationToken cancellationToken)
{
string fingerprint = User.GetFingerprint();

KeyExchangeDto keyExchange = await mediator.Send(
new SendKeyExchangeCommand(fingerprint, request.ToPublicKey, request.EncryptedPrivateKeyBase64),
cancellationToken);

return Ok(keyExchange.ToResponse());
}

[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<KeyExchangeResponse>))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetKeyExchangesAsync(
[FromQuery] string toPublicKey,
[FromQuery] DateTime? fromDate,
[FromQuery] DateTime? toDate,
CancellationToken cancellationToken)
{
string fingerprint = User.GetFingerprint();

IReadOnlyList<KeyExchangeDto> keyExchanges = await mediator.Send(
new GetKeyExchangesQuery(fingerprint, toPublicKey, fromDate, toDate),
cancellationToken);

return Ok(keyExchanges.Select(k => k.ToResponse()));
}
}
52 changes: 52 additions & 0 deletions API/Controllers/MessagesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Api.Contracts;
using Api.Extensions;
using Application.Commands;
using Application.DTOs;
using Application.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Api.Controllers;

[ApiController]
[Authorize]
[Route("/api/messages")]
public sealed class MessagesController(IMediator mediator) : ControllerBase
{
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MessageResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SendMessageAsync(
[FromBody] SendMessageRequest request,
CancellationToken cancellationToken)
{
string fingerprint = User.GetFingerprint();

MessageDto message = await mediator.Send(
new SendMessageCommand(fingerprint, request.ToPublicKey, request.EncryptedContentBase64, request.MessageHash),
cancellationToken);

return Ok(message.ToResponse());
}

[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<MessageResponse>))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetMessagesAsync(
[FromQuery] string toPublicKey,
[FromQuery] DateTime? fromDate,
[FromQuery] DateTime? toDate,
CancellationToken cancellationToken)
{
string fingerprint = User.GetFingerprint();

IReadOnlyList<MessageDto> messages = await mediator.Send(
new GetMessagesQuery(fingerprint, toPublicKey, fromDate, toDate),
cancellationToken);

return Ok(messages.Select(m => m.ToResponse()));
}
}
37 changes: 37 additions & 0 deletions API/Controllers/PublicKeysController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Api.Contracts;
using Application.DTOs;
using Application.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;

namespace Api.Controllers;

[ApiController]
[Authorize]
[Route("/api/public-keys")]
[EnableRateLimiting("search")]
public sealed class PublicKeysController(IMediator mediator) : ControllerBase
{
[HttpGet("search")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<PublicKeyProfileResponse>))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SearchAsync(
[FromQuery] string userName,
[FromQuery] uint? userTag,
[FromQuery] int? limit,
CancellationToken cancellationToken)
{
IReadOnlyList<PublicKeyProfileDto> results = await mediator.Send(
new SearchPublicKeysQuery(userName, userTag, limit),
cancellationToken);

return Ok(results.Select(p => new PublicKeyProfileResponse(
p.FingerprintSha512,
p.UserName,
p.UserTag,
Convert.ToBase64String(p.Der))));
}
}
Loading