WIP heartbeat and user presence
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful

This commit is contained in:
Mike 2024-12-15 16:06:14 +00:00
parent c3533187be
commit 81aa0ec1c0
40 changed files with 959 additions and 59 deletions

View file

@ -2,7 +2,7 @@
<RuleSet Name="Alveus StyleCop Rules" Description="Rules with IsEnabledByDefault = false are disabled." <RuleSet Name="Alveus StyleCop Rules" Description="Rules with IsEnabledByDefault = false are disabled."
ToolsVersion="14.0"> ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.SpecialRules"> <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.SpecialRules">
<Rule Id="SA0001" Action="Warning"/> <!-- XML comment analysis disabled --> <Rule Id="SA0001" Action="None"/> <!-- XML comment analysis disabled -->
<Rule Id="SA0002" Action="Warning"/> <!-- Invalid settings file --> <Rule Id="SA0002" Action="Warning"/> <!-- Invalid settings file -->
</Rules> </Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.SpacingRules"> <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.SpacingRules">

View file

@ -19,6 +19,7 @@
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="7.1.0"/> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="7.1.0"/>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.1.0"/> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.1.0"/>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.1.0"/> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.1.0"/>
<PackageReference Include="Toycloud.AspNetCore.Mvc.ModelBinding.BodyAndFormBinding" Version="1.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -14,7 +14,7 @@ namespace Astral.ApiServer.Controllers;
/// Base API controller with common methods. /// Base API controller with common methods.
/// </summary> /// </summary>
[Produces("application/json")] [Produces("application/json")]
[Consumes("application/x-www-form-urlencoded")] [Consumes("application/json")]
public class BaseApiController : ControllerBase public class BaseApiController : ControllerBase
{ {
/// <summary> /// <summary>
@ -24,8 +24,9 @@ public class BaseApiController : ControllerBase
protected IActionResult FailureResult() protected IActionResult FailureResult()
{ {
Response.StatusCode = (int)HttpStatusCode.BadRequest; Response.StatusCode = (int)HttpStatusCode.BadRequest;
return new JsonResult(new ResultModel return new JsonResult(new ResponseModel
{ {
Status = "failure",
Success = false, Success = false,
}); });
} }
@ -39,8 +40,9 @@ public class BaseApiController : ControllerBase
protected IActionResult FailureResult(string errorCode, string message) protected IActionResult FailureResult(string errorCode, string message)
{ {
Response.StatusCode = (int)HttpStatusCode.BadRequest; Response.StatusCode = (int)HttpStatusCode.BadRequest;
return new JsonResult(new ErrorResultModel return new JsonResult(new ErrorResponseModel
{ {
Status = "failure",
Error = errorCode, Error = errorCode,
Message = message, Message = message,
}); });
@ -55,6 +57,19 @@ public class BaseApiController : ControllerBase
return FailureResult(CoreErrorCodes.NoDataProvided, "Missing request body"); return FailureResult(CoreErrorCodes.NoDataProvided, "Missing request body");
} }
/// <summary>
/// Return a successful result.
/// </summary>
/// <param name="response">Instance of the response model.</param>
/// <typeparam name="TResponseModel">The response model type.</typeparam>
/// <returns>Instance of <see cref="IActionResult"/>.</returns>
protected IActionResult SuccessResult<TResponseModel>(TResponseModel response)
where TResponseModel : class
{
Response.StatusCode = (int)HttpStatusCode.OK;
return new JsonResult(new DataResponseModel<TResponseModel>(response));
}
/// <summary> /// <summary>
/// Return a success status with no data. /// Return a success status with no data.
/// </summary> /// </summary>
@ -62,8 +77,9 @@ public class BaseApiController : ControllerBase
protected IActionResult SuccessResult() protected IActionResult SuccessResult()
{ {
Response.StatusCode = (int)HttpStatusCode.OK; Response.StatusCode = (int)HttpStatusCode.OK;
return new JsonResult(new ResultModel return new JsonResult(new ResponseModel
{ {
Status = "success",
Success = true, Success = true,
}); });
} }
@ -80,8 +96,8 @@ public class BaseApiController : ControllerBase
foreach (var ip in value) foreach (var ip in value)
{ {
if (IPAddress.TryParse(ip, out var address) && if (IPAddress.TryParse(ip, out var address) &&
(address.AddressFamily is AddressFamily.InterNetwork address.AddressFamily is AddressFamily.InterNetwork
or AddressFamily.InterNetworkV6)) or AddressFamily.InterNetworkV6)
{ {
remoteIpAddress = address; remoteIpAddress = address;
break; break;

View file

@ -3,7 +3,8 @@
// </copyright> // </copyright>
using Astral.ApiServer.Constants; using Astral.ApiServer.Constants;
using Astral.ApiServer.Models; using Astral.ApiServer.Models.Requests;
using Astral.ApiServer.Models.Responses;
using Astral.Core.Constants; using Astral.Core.Constants;
using Astral.Services.Dtos; using Astral.Services.Dtos;
using Astral.Services.Interfaces; using Astral.Services.Interfaces;
@ -34,6 +35,7 @@ public class OAuthApiController : BaseApiController
/// </summary> /// </summary>
/// <param name="tokenGrantRequest">Instance of <see cref="TokenGrantRequestModel"/>.</param> /// <param name="tokenGrantRequest">Instance of <see cref="TokenGrantRequestModel"/>.</param>
[HttpPost("token")] [HttpPost("token")]
[Consumes("application/x-www-form-urlencoded")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> GrantToken([FromForm] TokenGrantRequestModel tokenGrantRequest) public async Task<IActionResult> GrantToken([FromForm] TokenGrantRequestModel tokenGrantRequest)
{ {
@ -65,7 +67,7 @@ public class OAuthApiController : BaseApiController
var result = await _authenticationService.AuthenticateSession(request); var result = await _authenticationService.AuthenticateSession(request);
return new JsonResult(new TokenGrantResultModel(result)); return new JsonResult(new TokenGrantResponseModel(result));
} }
return FailureResult(); return FailureResult();

View file

@ -0,0 +1,53 @@
// <copyright file="ServerInfoController.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.ApiServer.Models;
using Astral.ApiServer.Models.Responses;
using Astral.ApiServer.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Astral.ApiServer.Controllers;
/// <summary>
/// Get information about this instance.
/// </summary>
[Route("api")]
public class ServerInfoController : BaseApiController
{
private readonly MetaverseInfoResponseModel _metaverseInfo;
/// <summary>
/// Initializes a new instance of the <see cref="ServerInfoController"/> class.
/// </summary>
/// <param name="metaverse">Instance of <see cref="IOptions{MetaverseInfoOptions}"/>.</param>
public ServerInfoController(IOptions<MetaverseInfoOptions> metaverse)
{
var options = metaverse.Value;
_metaverseInfo = new MetaverseInfoResponseModel()
{
MetaverseName = options.Name,
MetaverseNickName = options.Nickname,
MetaverseUrl = options.ServerUrl,
IceServerUrl = options.IceServerUrl,
Version = new MetaverseVersionModel()
{
Version = options.Version,
Codename = options.Codename
}
};
}
/// <summary>
/// Get information about this metaverse instance.
/// </summary>
/// <returns>Instance of <see cref="MetaverseInfoResponseModel"/>.</returns>
[HttpGet("metaverse_info")]
[HttpGet("v1/metaverse_info")]
public IActionResult GetMetaverseInformation()
{
return new JsonResult(_metaverseInfo);
}
}

View file

@ -3,6 +3,8 @@
// </copyright> // </copyright>
using Astral.ApiServer.Models; using Astral.ApiServer.Models;
using Astral.ApiServer.Models.Requests;
using Astral.ApiServer.Models.Responses;
using Astral.Services.Interfaces; using Astral.Services.Interfaces;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -13,18 +15,24 @@ namespace Astral.ApiServer.Controllers.V1;
/// User api controller. /// User api controller.
/// </summary> /// </summary>
[Route("api/v1/user")] [Route("api/v1/user")]
[Consumes("application/json")]
[Authorize] [Authorize]
public class UserApiController : BaseApiController public class UserApiController : BaseApiController
{ {
private readonly IIdentityProvider _identityProvider; private readonly IIdentityProvider _identityProvider;
private readonly IUserPresenceService _userPresenceService;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserApiController"/> class. /// Initializes a new instance of the <see cref="UserApiController"/> class.
/// </summary> /// </summary>
/// <param name="identityProvider">Instance of <see cref="IIdentityProvider"/>.</param> /// <param name="identityProvider">Instance of <see cref="IIdentityProvider"/>.</param>
public UserApiController(IIdentityProvider identityProvider) /// <param name="userPresenceService">Instance of <see cref="IUserPresenceService"/>.</param>
public UserApiController(
IIdentityProvider identityProvider,
IUserPresenceService userPresenceService)
{ {
_identityProvider = identityProvider; _identityProvider = identityProvider;
_userPresenceService = userPresenceService;
} }
/// <summary> /// <summary>
@ -36,32 +44,83 @@ public class UserApiController : BaseApiController
var userId = _identityProvider.GetUserId(); var userId = _identityProvider.GetUserId();
var userName = _identityProvider.GetUserName(); var userName = _identityProvider.GetUserName();
return new JsonResult(new UserProfileResultModel() return SuccessResult(new UserProfileResponseModel()
{
User = new UserProfileModel()
{
AccountId = userId,
Username = userName,
XmppPassword = string.Empty,
DiscourseApiKey = string.Empty,
WalletId = Guid.Empty
}
});
}
/// <summary>
/// Receive a user's heartbeat.
/// </summary>
[Authorize]
[HttpPut("heartbeat")]
public async Task<IActionResult> ReceiveHeartbeat()
{
await _userPresenceService.HandleHeartbeat();
return SuccessResult();
}
/// <summary>
/// Receive a user's location heartbeat.
/// </summary>
[Authorize]
[HttpPut("location")]
public async Task<IActionResult> ReceiveLocation([FromBody] HeartbeatRootRequestModel heartbeatModel)
{
return SuccessResult();
}
/// <summary>
/// Receive the user's public key.
/// </summary>
[HttpPut("public_key")]
[Authorize]
[Consumes("multipart/form-data")]
public async Task<IActionResult> PutPublicKey()
{
var cert = HttpContext.Request.Form.Files.GetFile("public_key");
if (cert is null)
{ {
Success = true, return FailureResult();
AccountId = userId, }
Username = userName,
XmppPassword = string.Empty, await using (var stream = cert.OpenReadStream())
DiscourseApiKey = string.Empty {
}); await _userPresenceService.ConsumePublicKey(stream);
}
return SuccessResult();
} }
/// <summary> /// <summary>
/// Does nothing for now since I believe the locker feature is deprecated. /// Receive the user settings from the client.
/// TODO: Investigate this functionality.
/// </summary> /// </summary>
[Consumes("application/json")]
[HttpPost("locker")] [HttpPost("locker")]
public IActionResult PostLocker() public IActionResult PostLockerContents()
{ {
return SuccessResult(); return SuccessResult();
} }
/// <summary> /// <summary>
/// Does nothing for now since I believe the locker feature is deprecated. /// Return user settings to the client.
/// TODO: Investigate this functionality.
/// </summary> /// </summary>
[Produces("application/json")]
[HttpGet("locker")] [HttpGet("locker")]
public IActionResult GetLocker() public IActionResult GetLockerContents()
{ {
return SuccessResult(); var req = Request;
return SuccessResult(new object());
} }
} }

View file

@ -70,7 +70,7 @@ public class ExceptionMiddleware
{ {
context.Response.ContentType = "application/json"; context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)exception.HttpStatusCode; context.Response.StatusCode = (int)exception.HttpStatusCode;
await context.Response.WriteAsJsonAsync(new ErrorResultModel await context.Response.WriteAsJsonAsync(new ErrorResponseModel
{ {
Error = exception.ErrorCode, Error = exception.ErrorCode,
Message = exception.ErrorMessage Message = exception.ErrorMessage
@ -86,7 +86,7 @@ public class ExceptionMiddleware
{ {
context.Response.ContentType = "application/json"; context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity; context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity;
await context.Response.WriteAsJsonAsync(new ErrorResultModel await context.Response.WriteAsJsonAsync(new ErrorResponseModel
{ {
Error = CoreErrorCodes.ValidationError, Error = CoreErrorCodes.ValidationError,
Message = exception.Message Message = exception.Message
@ -101,7 +101,7 @@ public class ExceptionMiddleware
{ {
context.Response.ContentType = "application/json"; context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsJsonAsync(new ErrorResultModel await context.Response.WriteAsJsonAsync(new ErrorResponseModel
{ {
Error = ApiErrorCodes.UnknownError, Error = ApiErrorCodes.UnknownError,
Message = "Something went wrong. Try again later" Message = "Something went wrong. Try again later"

View file

@ -14,14 +14,19 @@ namespace Astral.ApiServer.Middleware;
public class StatusCodeMiddleware public class StatusCodeMiddleware
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly ILogger<StatusCodeMiddleware> _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StatusCodeMiddleware" /> class. /// Initializes a new instance of the <see cref="StatusCodeMiddleware" /> class.
/// </summary> /// </summary>
/// <param name="next">Instance of <see cref="RequestDelegate" />.</param> /// <param name="next">Instance of <see cref="RequestDelegate" />.</param>
public StatusCodeMiddleware(RequestDelegate next) /// <param name="logger">Instance of <see cref="ILogger{StatusCodeMiddleware}" />.</param>
public StatusCodeMiddleware(
RequestDelegate next,
ILogger<StatusCodeMiddleware> logger)
{ {
_next = next; _next = next;
_logger = logger;
} }
/// <summary> /// <summary>
@ -42,7 +47,7 @@ public class StatusCodeMiddleware
case 401: case 401:
context.Response.Headers.Clear(); context.Response.Headers.Clear();
context.Response.ContentType = "text/json"; context.Response.ContentType = "text/json";
await context.Response.WriteAsJsonAsync(new ErrorResultModel await context.Response.WriteAsJsonAsync(new ErrorResponseModel
{ {
Error = CoreErrorCodes.Unauthorized, Error = CoreErrorCodes.Unauthorized,
Message = "You're not authorized to do that" Message = "You're not authorized to do that"
@ -52,7 +57,8 @@ public class StatusCodeMiddleware
case 404: case 404:
context.Response.Headers.Clear(); context.Response.Headers.Clear();
context.Response.ContentType = "text/json"; context.Response.ContentType = "text/json";
await context.Response.WriteAsJsonAsync(new ErrorResultModel _logger.LogWarning("Request to non-existing endpoint: {endpoint}", context.Request.Path);
await context.Response.WriteAsJsonAsync(new ErrorResponseModel
{ {
Error = ApiErrorCodes.UnknownMethod, Error = ApiErrorCodes.UnknownMethod,
Message = "Unknown method" Message = "Unknown method"
@ -62,7 +68,7 @@ public class StatusCodeMiddleware
case 405: case 405:
context.Response.Headers.Clear(); context.Response.Headers.Clear();
context.Response.ContentType = "text/json"; context.Response.ContentType = "text/json";
await context.Response.WriteAsJsonAsync(new ErrorResultModel await context.Response.WriteAsJsonAsync(new ErrorResponseModel
{ {
Error = ApiErrorCodes.IllegalMethod, Error = ApiErrorCodes.IllegalMethod,
Message = "Illegal method" Message = "Illegal method"
@ -72,7 +78,7 @@ public class StatusCodeMiddleware
case 415: case 415:
context.Response.Headers.Clear(); context.Response.Headers.Clear();
context.Response.ContentType = "text/json"; context.Response.ContentType = "text/json";
await context.Response.WriteAsJsonAsync(new ErrorResultModel await context.Response.WriteAsJsonAsync(new ErrorResponseModel
{ {
Error = ApiErrorCodes.UnsupportedBody, Error = ApiErrorCodes.UnsupportedBody,
Message = "Unsupported body/media type" Message = "Unsupported body/media type"
@ -82,7 +88,7 @@ public class StatusCodeMiddleware
case 500: case 500:
context.Response.Headers.Clear(); context.Response.Headers.Clear();
context.Response.ContentType = "text/json"; context.Response.ContentType = "text/json";
await context.Response.WriteAsJsonAsync(new ErrorResultModel await context.Response.WriteAsJsonAsync(new ErrorResponseModel
{ {
Error = ApiErrorCodes.UnknownError, Error = ApiErrorCodes.UnknownError,
Message = "Unknown error" Message = "Unknown error"

View file

@ -0,0 +1,30 @@
// <copyright file="DataResponseModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using System.Text.Json.Serialization;
namespace Astral.ApiServer.Models.Common;
/// <summary>
/// Success with data response.
/// </summary>
public class DataResponseModel<TDataType> : ResponseModel
{
/// <summary>
/// Initializes a new instance of the <see cref="DataResponseModel{TDataType}"/> class.
/// </summary>
/// <param name="data">Instance of <see cref="TDataType"/>.</param>
public DataResponseModel(TDataType data)
{
Status = "success";
Success = true;
Data = data;
}
/// <summary>
/// The data response.
/// </summary>
[JsonPropertyName("data")]
public TDataType Data { get; set; }
}

View file

@ -1,4 +1,4 @@
// <copyright file="ErrorResultModel.cs" company="alveus.dev"> // <copyright file="ErrorResponseModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright> // </copyright>
@ -9,7 +9,7 @@ namespace Astral.ApiServer.Models.Common;
/// <summary> /// <summary>
/// Error result model. /// Error result model.
/// </summary> /// </summary>
public class ErrorResultModel : ResultModel public class ErrorResponseModel : ResponseModel
{ {
/// <summary> /// <summary>
/// Error code. /// Error code.

View file

@ -1,4 +1,4 @@
// <copyright file="ResultModel.cs" company="alveus.dev"> // <copyright file="ResponseModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright> // </copyright>
@ -9,8 +9,14 @@ namespace Astral.ApiServer.Models.Common;
/// <summary> /// <summary>
/// Generic result model. /// Generic result model.
/// </summary> /// </summary>
public class ResultModel public class ResponseModel
{ {
/// <summary>
/// Interface requires a string to represent success or failure rather than a boolean.
/// </summary>
[JsonPropertyName("status")]
public string Status { get; set; }
/// <summary> /// <summary>
/// Indicate success. /// Indicate success.
/// </summary> /// </summary>

View file

@ -0,0 +1,61 @@
// <copyright file="HeartbeatModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using System.Text.Json.Serialization;
namespace Astral.ApiServer.Models;
/// <summary>
/// Heartbeat contents.
/// </summary>
public class HeartbeatModel
{
/// <summary>
/// True if connected.
/// </summary>
[JsonPropertyName("connected")]
public bool Connected { get; set; }
/// <summary>
/// Path.
/// </summary>
[JsonPropertyName("path")]
public string Path { get; set; }
/// <summary>
/// Domain id.
/// </summary>
[JsonPropertyName("domain_id")]
public Guid DomainId { get; set; }
/// <summary>
/// Place id.
/// </summary>
[JsonPropertyName("place_id")]
public Guid PlaceId { get; set; }
/// <summary>
/// Network address.
/// </summary>
[JsonPropertyName("network_address")]
public string NetworkAddress { get; set; }
/// <summary>
/// Network port.
/// </summary>
[JsonPropertyName("network_port")]
public int NetworkPort { get; set; }
/// <summary>
/// Node id.
/// </summary>
[JsonPropertyName("node_id")]
public Guid NodeId { get; set; }
/// <summary>
/// Availability.
/// </summary>
[JsonPropertyName("availability")]
public string Availability { get; set; }
}

View file

@ -0,0 +1,25 @@
// <copyright file="MetaverseVersionModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using System.Text.Json.Serialization;
namespace Astral.ApiServer.Models;
/// <summary>
/// Version information about this instance.
/// </summary>
public class MetaverseVersionModel
{
/// <summary>
/// Version string.
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; }
/// <summary>
/// Codename.
/// </summary>
[JsonPropertyName("codename")]
public string Codename { get; set; }
}

View file

@ -0,0 +1,19 @@
// <copyright file="HeartbeatRootRequestModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using System.Text.Json.Serialization;
namespace Astral.ApiServer.Models.Requests;
/// <summary>
/// Heartbeat request model.
/// </summary>
public class HeartbeatRootRequestModel
{
/// <summary>
/// Heartbeat location information.
/// </summary>
[JsonPropertyName("location")]
public HeartbeatModel Location { get; set; }
}

View file

@ -4,7 +4,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Astral.ApiServer.Models; namespace Astral.ApiServer.Models.Requests;
/// <summary> /// <summary>
/// Oauth token grant request. /// Oauth token grant request.

View file

@ -0,0 +1,19 @@
// <copyright file="HeartbeatResponseModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using System.Text.Json.Serialization;
namespace Astral.ApiServer.Models.Responses;
/// <summary>
/// The response given from a heartbeat request.
/// </summary>
public class HeartbeatResponseModel
{
/// <summary>
/// Session id.
/// </summary>
[JsonPropertyName("session_id")]
public Guid SessionId { get; set; }
}

View file

@ -0,0 +1,43 @@
// <copyright file="MetaverseInfoResponseModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using System.Text.Json.Serialization;
namespace Astral.ApiServer.Models.Responses;
/// <summary>
/// Information about this instance.
/// </summary>
public class MetaverseInfoResponseModel
{
/// <summary>
/// Metaverse name.
/// </summary>
[JsonPropertyName("metaverse_name")]
public string MetaverseName { get; set; }
/// <summary>
/// Metaverse nickname.
/// </summary>
[JsonPropertyName("metaverse_nick_name")]
public string MetaverseNickName { get; set; }
/// <summary>
/// Url of this server.
/// </summary>
[JsonPropertyName("metaverse_url")]
public string MetaverseUrl { get; set; }
/// <summary>
/// ICE server url.
/// </summary>
[JsonPropertyName("ice_server_url")]
public string IceServerUrl { get; set; }
/// <summary>
/// Version information.
/// </summary>
[JsonPropertyName("metaverse_server_version")]
public MetaverseVersionModel Version { get; set; }
}

View file

@ -1,36 +1,28 @@
// <copyright file="TokenGrantResultModel.cs" company="alveus.dev"> // <copyright file="TokenGrantResponseModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright> // </copyright>
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Astral.ApiServer.Models.Common;
using Astral.Core.Extensions; using Astral.Core.Extensions;
using Astral.Services.Dtos; using Astral.Services.Dtos;
namespace Astral.ApiServer.Models; namespace Astral.ApiServer.Models.Responses;
/// <summary> /// <summary>
/// OAuth Grant Request Response. /// OAuth Grant Request Response.
/// </summary> /// </summary>
public class TokenGrantResultModel : ResultModel // ReSharper disable UnusedAutoPropertyAccessor.Global
public class TokenGrantResponseModel
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TokenGrantResultModel"/> class. /// Initializes a new instance of the <see cref="TokenGrantResponseModel"/> class.
/// </summary>
public TokenGrantResultModel()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TokenGrantResultModel"/> class.
/// </summary> /// </summary>
/// <param name="sessionDto">Instance of <see cref="SessionDto"/> to create from.</param> /// <param name="sessionDto">Instance of <see cref="SessionDto"/> to create from.</param>
public TokenGrantResultModel(SessionDto sessionDto) public TokenGrantResponseModel(SessionDto sessionDto)
{ {
Success = true;
AccessToken = sessionDto.AccessToken; AccessToken = sessionDto.AccessToken;
CreatedAt = sessionDto.CreatedAt.Ticks; CreatedAt = ((DateTimeOffset)sessionDto.CreatedAt).ToUnixTimeSeconds();
ExpiresIn = sessionDto.AccessTokenExpires.Ticks; ExpiresIn = ((DateTimeOffset)sessionDto.AccessTokenExpires).ToUnixTimeSeconds();
RefreshToken = sessionDto.RefreshToken; RefreshToken = sessionDto.RefreshToken;
Scope = sessionDto.Scope.ToString().ToLowerInvariant(); Scope = sessionDto.Scope.ToString().ToLowerInvariant();
AccountId = sessionDto.UserId; AccountId = sessionDto.UserId;

View file

@ -0,0 +1,19 @@
// <copyright file="UserProfileResponseModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using System.Text.Json.Serialization;
namespace Astral.ApiServer.Models.Responses;
/// <summary>
/// User response model.
/// </summary>
public class UserProfileResponseModel
{
/// <summary>
/// User profile.
/// </summary>
[JsonPropertyName("user")]
public UserProfileModel User { get; set; }
}

View file

@ -1,16 +1,15 @@
// <copyright file="UserProfileResultModel.cs" company="alveus.dev"> // <copyright file="UserProfileModel.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright> // </copyright>
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Astral.ApiServer.Models.Common;
namespace Astral.ApiServer.Models; namespace Astral.ApiServer.Models;
/// <summary> /// <summary>
/// User profile request result. /// User profile request result.
/// </summary> /// </summary>
public class UserProfileResultModel : ResultModel public class UserProfileModel
{ {
/// <summary> /// <summary>
/// Account id (Even used?). /// Account id (Even used?).
@ -35,4 +34,10 @@ public class UserProfileResultModel : ResultModel
/// </summary> /// </summary>
[JsonPropertyName("xmpp_password")] [JsonPropertyName("xmpp_password")]
public string XmppPassword { get; set; } public string XmppPassword { get; set; }
/// <summary>
/// Wallet id (Even used?).
/// </summary>
[JsonPropertyName("wallet_id")]
public Guid WalletId { get; set; }
} }

View file

@ -0,0 +1,46 @@
// <copyright file="MetaverseInfoOptions.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
namespace Astral.ApiServer.Options;
/// <summary>
/// Metaverse information options.
/// </summary>
public class MetaverseInfoOptions
{
/// <summary>
/// Metaverse name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Metaverse nickname.
/// </summary>
public string Nickname { get; set; }
/// <summary>
/// Metaverse server url.
/// </summary>
public string ServerUrl { get; set; }
/// <summary>
/// Ice server url.
/// </summary>
public string IceServerUrl { get; set; }
/// <summary>
/// Dashboard url.
/// </summary>
public string DashboardUrl { get; set; }
/// <summary>
/// Metaverse version.
/// </summary>
public string Version { get; set; }
/// <summary>
/// Metaverse codename.
/// </summary>
public string Codename { get; set; }
}

View file

@ -5,6 +5,7 @@
using Astral.ApiServer.Extensions; using Astral.ApiServer.Extensions;
using Astral.ApiServer.HostedService; using Astral.ApiServer.HostedService;
using Astral.ApiServer.Middleware; using Astral.ApiServer.Middleware;
using Astral.ApiServer.Options;
using Astral.Core.Options; using Astral.Core.Options;
using Astral.Services.Options; using Astral.Services.Options;
using Astral.Services.Profiles; using Astral.Services.Profiles;
@ -85,6 +86,7 @@ builder.Services.AddAutoMapper(typeof(AutomapperProfile).Assembly);
// Setup configuration. // Setup configuration.
builder.Services.Configure<DatabaseOptions>(builder.Configuration.GetSection("Database")); builder.Services.Configure<DatabaseOptions>(builder.Configuration.GetSection("Database"));
builder.Services.Configure<PwdHashOptions>(builder.Configuration.GetSection("PwdHash")); builder.Services.Configure<PwdHashOptions>(builder.Configuration.GetSection("PwdHash"));
builder.Services.Configure<MetaverseInfoOptions>(builder.Configuration.GetSection("Metaverse"));
builder.Services.Configure<InitialUserOptions>(builder.Configuration.GetSection("InitialUser")); builder.Services.Configure<InitialUserOptions>(builder.Configuration.GetSection("InitialUser"));
builder.Services.Configure<RegistrationOptions>(builder.Configuration.GetSection("Registration")); builder.Services.Configure<RegistrationOptions>(builder.Configuration.GetSection("Registration"));
builder.Services.Configure<EmailDomainBlacklistOptions>( builder.Services.Configure<EmailDomainBlacklistOptions>(

View file

@ -16,6 +16,15 @@
"SaltSize": 64, "SaltSize": 64,
"HashSize": 128 "HashSize": 128
}, },
"Metaverse": {
"Name": "Astral Directory Services",
"Nickname": "Astral",
"ServerUrl": "http://localhost:5000",
"IceServerUrl": "localhost",
"DashboardUrl": "localhost",
"Version": "0.1 alpha",
"Codename": "astral"
},
"InitialUser": { "InitialUser": {
"Username": "admin", "Username": "admin",
"Email": "admin@changeme.com", "Email": "admin@changeme.com",

View file

@ -0,0 +1,31 @@
// <copyright file="Discoverability.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
namespace Astral.Core.Constants;
/// <summary>
/// User's discoverability/availability.
/// </summary>
public enum Discoverability
{
/// <summary>
/// Discoverable to none.
/// </summary>
None,
/// <summary>
/// Discoverable only to friends.
/// </summary>
Friends,
/// <summary>
/// Discoverable to connections.
/// </summary>
Connections,
/// <summary>
/// Discoverable to all.
/// </summary>
All
}

View file

@ -0,0 +1,27 @@
// <copyright file="UserLocker.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.Core.Attributes.EntityAnnotation;
namespace Astral.Core.Entities;
/// <summary>
/// User locker entity.
/// </summary>
[TableMapping("userLocker")]
public class UserLocker
{
/// <summary>
/// User id.
/// </summary>
[ColumnMapping("id")]
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// Locker contents.
/// </summary>
[ColumnMapping("contents")]
public string Contents { get; set; }
}

View file

@ -0,0 +1,94 @@
// <copyright file="UserPresence.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.Core.Attributes.EntityAnnotation;
using Astral.Core.Constants;
namespace Astral.Core.Entities;
/// <summary>
/// User presence entity.
/// </summary>
[TableMapping("userPresence")]
public class UserPresence
{
/// <summary>
/// The user id this belongs to.
/// </summary>
[PrimaryKey]
[ColumnMapping("id")]
public Guid Id { get; set; }
/// <summary>
/// When the entity was created.
/// </summary>
[ColumnMapping("createdAt")]
public DateTime CreatedAt { get; set; }
/// <summary>
/// When the entity was last updated.
/// </summary>
[ColumnMapping("updatedAt")]
public DateTime UpdatedAt { get; set; }
/// <summary>
/// True if connected.
/// </summary>
[ColumnMapping("connected")]
public bool? Connected { get; set; }
/// <summary>
/// Current domain id.
/// </summary>
[ColumnMapping("domainId")]
public Guid? DomainId { get; set; }
/// <summary>
/// Current place id.
/// </summary>
[ColumnMapping("placeId")]
public Guid? PlaceId { get; set; }
/// <summary>
/// Current network address.
/// </summary>
[ColumnMapping("networkAddress")]
public string NetworkAddress { get; set; }
/// <summary>
/// Current network port.
/// </summary>
[ColumnMapping("networkPort")]
public int? NetworkPort { get; set; }
/// <summary>
/// Current public key.
/// </summary>
[ColumnMapping("publicKey")]
public string PublicKey { get; set; }
/// <summary>
/// Current path.
/// </summary>
[ColumnMapping("path")]
public string Path { get; set; }
/// <summary>
/// Last heartbeat received.
/// </summary>
[ColumnMapping("lastHeartbeat")]
public DateTime LastHeartbeat { get; set; }
/// <summary>
/// Node id.
/// </summary>
[ColumnMapping("nodeId")]
public Guid? NodeId { get; set; }
/// <summary>
/// Discoverability.
/// </summary>
[ColumnMapping("availability")]
public Discoverability Availability { get; set; }
}

View file

@ -0,0 +1,29 @@
// <copyright file="StreamExtensions.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
namespace Astral.Core.Extensions;
/// <summary>
/// <see cref="Stream"/> extensions.
/// </summary>
public static class StreamExtensions
{
/// <summary>
/// Return a byte array of the contents of the stream.
/// </summary>
/// <param name="stream">Instance of <see cref="Stream"/>.</param>
/// <returns>Collection of bytes.</returns>
public static byte[] ToByteArray(this Stream stream)
{
stream.Position = 0;
var buffer = new byte[stream.Length];
for (var totalBytesCopied = 0; totalBytesCopied < stream.Length;)
{
totalBytesCopied +=
stream.Read(buffer, totalBytesCopied, Convert.ToInt32(stream.Length) - totalBytesCopied);
}
return buffer;
}
}

View file

@ -0,0 +1,14 @@
// <copyright file="IUserLockerRepository.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.Core.Entities;
namespace Astral.Core.RepositoryInterfaces;
/// <summary>
/// <see cref="UserLocker"/> Repository.
/// </summary>
public interface IUserLockerRepository : IGenericRepository<UserLocker, Guid>
{
}

View file

@ -0,0 +1,14 @@
// <copyright file="IUserPresenceRepository.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.Core.Entities;
namespace Astral.Core.RepositoryInterfaces;
/// <summary>
/// <see cref="UserPresence"/> repository.
/// </summary>
public interface IUserPresenceRepository : IGenericRepository<UserPresence, Guid>
{
}

View file

@ -0,0 +1,7 @@
CREATE TABLE userLocker
(
id UUID UNIQUE PRIMARY KEY REFERENCES users (id) ON DELETE CASCADE,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
contents JSON DEFAULT NULL
);

View file

@ -0,0 +1,22 @@
CREATE TABLE userPresence
(
id UUID PRIMARY KEY REFERENCES users (id) ON DELETE CASCADE,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
connected BOOL DEFAULT FALSE,
domainId UUID DEFAULT NULL,
placeId UUID DEFAULT NULL,
networkAddress TEXT DEFAULT NULL,
networkPort INT DEFAULT NULL,
nodeId UUID DEFAULT NULL,
availability SMALLINT DEFAULT NULL,
publicKey TEXT DEFAULT NULL,
path TEXT DEFAULT '',
lastHeartbeat TIMESTAMP DEFAULT NULL
);
CREATE TRIGGER userPresence_updated_at
BEFORE UPDATE
ON userPresence
FOR EACH ROW
EXECUTE PROCEDURE updated_at_timestamp();

View file

@ -0,0 +1,24 @@
// <copyright file="UserLockerRepository.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.Core.Entities;
using Astral.Core.Infrastructure;
using Astral.Core.RepositoryInterfaces;
using Injectio.Attributes;
namespace Astral.DAL.Repositories;
/// <inheritdoc cref="IUserLockerRepository" />
[RegisterScoped]
public class UserLockerRepository : BaseRepository<UserLocker, Guid>, IUserLockerRepository
{
/// <summary>
/// Initializes a new instance of the <see cref="UserLockerRepository"/> class.
/// </summary>
/// <param name="db">Instance of <see cref="IDbConnectionProvider"/>.</param>
public UserLockerRepository(IDbConnectionProvider db)
: base(db)
{
}
}

View file

@ -0,0 +1,24 @@
// <copyright file="UserPresenceRepository.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.Core.Entities;
using Astral.Core.Infrastructure;
using Astral.Core.RepositoryInterfaces;
using Injectio.Attributes;
namespace Astral.DAL.Repositories;
/// <inheritdoc cref="IUserPresenceRepository" />
[RegisterScoped]
public class UserPresenceRepository : BaseRepository<UserPresence, Guid>, IUserPresenceRepository
{
/// <summary>
/// Initializes a new instance of the <see cref="UserPresenceRepository"/> class.
/// </summary>
/// <param name="db">Instance of <see cref="IDbConnectionProvider"/>.</param>
public UserPresenceRepository(IDbConnectionProvider db)
: base(db)
{
}
}

View file

@ -0,0 +1,51 @@
// <copyright file="UserLocationHeartbeatDto.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
namespace Astral.Services.Dtos;
/// <summary>
/// Heartbeat containing location data.
/// </summary>
public class UserLocationHeartbeatDto
{
/// <summary>
/// True if connected.
/// </summary>
public bool Connected { get; set; }
/// <summary>
/// Path.
/// </summary>
public string Path { get; set; }
/// <summary>
/// Domain id.
/// </summary>
public Guid DomainId { get; set; }
/// <summary>
/// Place id.
/// </summary>
public Guid PlaceId { get; set; }
/// <summary>
/// Network address.
/// </summary>
public string NetworkAddress { get; set; }
/// <summary>
/// Network port.
/// </summary>
public int NetworkPort { get; set; }
/// <summary>
/// Node id.
/// </summary>
public Guid NodeId { get; set; }
/// <summary>
/// Availability.
/// </summary>
public string Availability { get; set; }
}

View file

@ -0,0 +1,30 @@
// <copyright file="IUserPresenceService.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.Services.Dtos;
namespace Astral.Services.Interfaces;
/// <summary>
/// User's presence service.
/// </summary>
public interface IUserPresenceService
{
/// <summary>
/// Process a user's heartbeat.
/// </summary>
Task HandleHeartbeat();
/// <summary>
/// Process a user's heartbeat with location.
/// </summary>
/// <param name="heartbeat">Instance of <see cref="UserLocationHeartbeatDto"/>.</param>
Task ConsumeLocationHeartbeat(UserLocationHeartbeatDto heartbeat);
/// <summary>
/// Update the user's public key.
/// </summary>
/// <param name="publicKey">Stream containing the public key.</param>
Task ConsumePublicKey(Stream publicKey);
}

View file

@ -18,6 +18,7 @@ using Astral.Services.Exceptions;
using Astral.Services.Interfaces; using Astral.Services.Interfaces;
using FluentValidation; using FluentValidation;
using Injectio.Attributes; using Injectio.Attributes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -35,6 +36,7 @@ public class AuthenticationService : IAuthenticationService
private readonly IRefreshTokenRepository _refreshTokenRepository; private readonly IRefreshTokenRepository _refreshTokenRepository;
private readonly IValidator<PasswordAuthenticateDto> _passwordAuthValidator; private readonly IValidator<PasswordAuthenticateDto> _passwordAuthValidator;
private readonly JwtOptions _jwtOptions; private readonly JwtOptions _jwtOptions;
private readonly ILogger<AuthenticationService> _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AuthenticationService"/> class. /// Initializes a new instance of the <see cref="AuthenticationService"/> class.
@ -45,13 +47,15 @@ public class AuthenticationService : IAuthenticationService
/// <param name="refreshTokenRepository">Instance of <see cref="IRefreshTokenRepository"/>.</param> /// <param name="refreshTokenRepository">Instance of <see cref="IRefreshTokenRepository"/>.</param>
/// <param name="passwordAuthValidator">Instance of <see cref="IValidator{PasswordAuthenticateDto}"/>.</param> /// <param name="passwordAuthValidator">Instance of <see cref="IValidator{PasswordAuthenticateDto}"/>.</param>
/// <param name="jwtOptions">Instance of <see cref="IOptions{JwtOptions}"/>.</param> /// <param name="jwtOptions">Instance of <see cref="IOptions{JwtOptions}"/>.</param>
/// <param name="logger">Instance of <see cref="ILogger{AuthenticationService}"/>.</param>
public AuthenticationService( public AuthenticationService(
ICryptographyService cryptographyService, ICryptographyService cryptographyService,
ITransactionProvider transactionProvider, ITransactionProvider transactionProvider,
IUserRepository userRepository, IUserRepository userRepository,
IRefreshTokenRepository refreshTokenRepository, IRefreshTokenRepository refreshTokenRepository,
IValidator<PasswordAuthenticateDto> passwordAuthValidator, IValidator<PasswordAuthenticateDto> passwordAuthValidator,
IOptions<JwtOptions> jwtOptions) IOptions<JwtOptions> jwtOptions,
ILogger<AuthenticationService> logger)
{ {
_cryptographyService = cryptographyService; _cryptographyService = cryptographyService;
_transactionProvider = transactionProvider; _transactionProvider = transactionProvider;
@ -59,6 +63,7 @@ public class AuthenticationService : IAuthenticationService
_refreshTokenRepository = refreshTokenRepository; _refreshTokenRepository = refreshTokenRepository;
_passwordAuthValidator = passwordAuthValidator; _passwordAuthValidator = passwordAuthValidator;
_jwtOptions = jwtOptions.Value; _jwtOptions = jwtOptions.Value;
_logger = logger;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -102,6 +107,7 @@ public class AuthenticationService : IAuthenticationService
// Generate token. // Generate token.
var result = await GenerateAccessTokens(user, passwordAuthenticateDto.Scope, passwordAuthenticateDto.IpAddress); var result = await GenerateAccessTokens(user, passwordAuthenticateDto.Scope, passwordAuthenticateDto.IpAddress);
_logger.LogInformation("Granted authorisation to user {user}", user.Username);
transaction.Commit(); transaction.Commit();
return result; return result;
} }

View file

@ -91,7 +91,8 @@ public class CryptographyService : ICryptographyService
rsa.ImportRSAPublicKey(pkcs1Key, out bytesRead); rsa.ImportRSAPublicKey(pkcs1Key, out bytesRead);
break; break;
} }
var pem = "";
var pem = string.Empty;
if (bytesRead == 0) if (bytesRead == 0)
{ {
_logger.LogError( _logger.LogError(

View file

@ -0,0 +1,105 @@
// <copyright file="UserPresenceService.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
using Astral.Core.Entities;
using Astral.Core.Extensions;
using Astral.Core.RepositoryInterfaces;
using Astral.Services.Constants;
using Astral.Services.Dtos;
using Astral.Services.Exceptions;
using Astral.Services.Interfaces;
using Injectio.Attributes;
namespace Astral.Services.Services;
/// <inheritdoc />
[RegisterScoped]
public class UserPresenceService : IUserPresenceService
{
private readonly IIdentityProvider _identityProvider;
private readonly ICryptographyService _cryptographyService;
private readonly IUserPresenceRepository _userPresenceRepository;
/// <summary>
/// Initializes a new instance of the <see cref="UserPresenceService"/> class.
/// </summary>
/// <param name="identityProvider">Instance of <see cref="IIdentityProvider"/>.</param>
/// <param name="cryptographyService">Instance of <see cref="ICryptographyService"/>.</param>
/// <param name="userPresenceRepository">Instance of <see cref="IUserPresenceRepository"/>.</param>
public UserPresenceService(
IIdentityProvider identityProvider,
ICryptographyService cryptographyService,
IUserPresenceRepository userPresenceRepository)
{
_identityProvider = identityProvider;
_cryptographyService = cryptographyService;
_userPresenceRepository = userPresenceRepository;
}
/// <inheritdoc />
public async Task HandleHeartbeat()
{
var userId = _identityProvider.GetUserId();
if (userId == Guid.Empty)
{
throw new UnauthorizedException();
}
var heartbeat = await _userPresenceRepository.FindByIdAsync(userId);
if (heartbeat is null)
{
heartbeat = new UserPresence()
{
Id = userId,
LastHeartbeat = DateTime.UtcNow
};
await _userPresenceRepository.AddAsync(heartbeat);
}
else
{
heartbeat.LastHeartbeat = DateTime.UtcNow;
await _userPresenceRepository.UpdateAsync(heartbeat);
}
}
/// <inheritdoc />
public async Task ConsumeLocationHeartbeat(UserLocationHeartbeatDto heartbeat)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public async Task ConsumePublicKey(Stream publicKey)
{
var userId = _identityProvider.GetUserId();
if (userId == Guid.Empty)
{
throw new UnauthorizedException();
}
var key = _cryptographyService.ConvertPublicKey(publicKey.ToByteArray(), PublicKeyType.Pkcs1PublicKey);
var heartbeat = await _userPresenceRepository.FindByIdAsync(userId);
if (heartbeat is null)
{
heartbeat = new UserPresence()
{
Id = userId,
LastHeartbeat = DateTime.UtcNow,
PublicKey = key
};
await _userPresenceRepository.AddAsync(heartbeat);
}
else
{
heartbeat.LastHeartbeat = DateTime.UtcNow;
heartbeat.PublicKey = key;
await _userPresenceRepository.UpdateAsync(heartbeat);
}
}
}

View file

@ -33,6 +33,7 @@ public class UserService : IUserService
private readonly IUserGroupService _userGroupService; private readonly IUserGroupService _userGroupService;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IUserProfileRepository _userProfileRepository; private readonly IUserProfileRepository _userProfileRepository;
private readonly IUserLockerRepository _userLockerRepository;
private readonly ILogger<UserService> _logger; private readonly ILogger<UserService> _logger;
/// <summary> /// <summary>
@ -46,6 +47,7 @@ public class UserService : IUserService
/// <param name="transactionProvider">Instance of <see cref="ITransactionProvider" />.</param> /// <param name="transactionProvider">Instance of <see cref="ITransactionProvider" />.</param>
/// <param name="userRepository">Instance of <see cref="IUserRepository" />.</param> /// <param name="userRepository">Instance of <see cref="IUserRepository" />.</param>
/// <param name="userProfileRepository">Instance of <see cref="IUserProfileRepository" />.</param> /// <param name="userProfileRepository">Instance of <see cref="IUserProfileRepository" />.</param>
/// <param name="userLockerRepository">Instance of <see cref="IUserLockerRepository" />.</param>
/// <param name="logger">Instance of <see cref="ILogger" />.</param> /// <param name="logger">Instance of <see cref="ILogger" />.</param>
public UserService( public UserService(
IUserRepository userRepository, IUserRepository userRepository,
@ -56,6 +58,7 @@ public class UserService : IUserService
IOptions<RegistrationOptions> registrationConfig, IOptions<RegistrationOptions> registrationConfig,
ITransactionProvider transactionProvider, ITransactionProvider transactionProvider,
IUserProfileRepository userProfileRepository, IUserProfileRepository userProfileRepository,
IUserLockerRepository userLockerRepository,
ILogger<UserService> logger) ILogger<UserService> logger)
{ {
_userRepository = userRepository; _userRepository = userRepository;
@ -66,6 +69,7 @@ public class UserService : IUserService
_registrationConfiguration = registrationConfig.Value; _registrationConfiguration = registrationConfig.Value;
_transactionProvider = transactionProvider; _transactionProvider = transactionProvider;
_userProfileRepository = userProfileRepository; _userProfileRepository = userProfileRepository;
_userLockerRepository = userLockerRepository;
_logger = logger; _logger = logger;
} }

View file

@ -1 +1,5 @@
global using Xunit; // <copyright file="GlobalUsings.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
global using Xunit;