Merge pull request 'heartbeat and user presence' (#5) from 2024-12-14-heartbeat-and-presence into main
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #5
This commit is contained in:
commit
77fd12a796
42 changed files with 1101 additions and 59 deletions
|
@ -2,7 +2,7 @@
|
|||
<RuleSet Name="Alveus StyleCop Rules" Description="Rules with IsEnabledByDefault = false are disabled."
|
||||
ToolsVersion="14.0">
|
||||
<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 -->
|
||||
</Rules>
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.SpacingRules">
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="7.1.0"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" 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>
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace Astral.ApiServer.Controllers;
|
|||
/// Base API controller with common methods.
|
||||
/// </summary>
|
||||
[Produces("application/json")]
|
||||
[Consumes("application/x-www-form-urlencoded")]
|
||||
[Consumes("application/json")]
|
||||
public class BaseApiController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -24,8 +24,9 @@ public class BaseApiController : ControllerBase
|
|||
protected IActionResult FailureResult()
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
return new JsonResult(new ResultModel
|
||||
return new JsonResult(new ResponseModel
|
||||
{
|
||||
Status = "failure",
|
||||
Success = false,
|
||||
});
|
||||
}
|
||||
|
@ -39,8 +40,9 @@ public class BaseApiController : ControllerBase
|
|||
protected IActionResult FailureResult(string errorCode, string message)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
return new JsonResult(new ErrorResultModel
|
||||
return new JsonResult(new ErrorResponseModel
|
||||
{
|
||||
Status = "failure",
|
||||
Error = errorCode,
|
||||
Message = message,
|
||||
});
|
||||
|
@ -55,6 +57,19 @@ public class BaseApiController : ControllerBase
|
|||
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>
|
||||
/// Return a success status with no data.
|
||||
/// </summary>
|
||||
|
@ -62,8 +77,9 @@ public class BaseApiController : ControllerBase
|
|||
protected IActionResult SuccessResult()
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.OK;
|
||||
return new JsonResult(new ResultModel
|
||||
return new JsonResult(new ResponseModel
|
||||
{
|
||||
Status = "success",
|
||||
Success = true,
|
||||
});
|
||||
}
|
||||
|
@ -80,8 +96,8 @@ public class BaseApiController : ControllerBase
|
|||
foreach (var ip in value)
|
||||
{
|
||||
if (IPAddress.TryParse(ip, out var address) &&
|
||||
(address.AddressFamily is AddressFamily.InterNetwork
|
||||
or AddressFamily.InterNetworkV6))
|
||||
address.AddressFamily is AddressFamily.InterNetwork
|
||||
or AddressFamily.InterNetworkV6)
|
||||
{
|
||||
remoteIpAddress = address;
|
||||
break;
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
// </copyright>
|
||||
|
||||
using Astral.ApiServer.Constants;
|
||||
using Astral.ApiServer.Models;
|
||||
using Astral.ApiServer.Models.Requests;
|
||||
using Astral.ApiServer.Models.Responses;
|
||||
using Astral.Core.Constants;
|
||||
using Astral.Services.Dtos;
|
||||
using Astral.Services.Interfaces;
|
||||
|
@ -34,6 +35,7 @@ public class OAuthApiController : BaseApiController
|
|||
/// </summary>
|
||||
/// <param name="tokenGrantRequest">Instance of <see cref="TokenGrantRequestModel"/>.</param>
|
||||
[HttpPost("token")]
|
||||
[Consumes("application/x-www-form-urlencoded")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> GrantToken([FromForm] TokenGrantRequestModel tokenGrantRequest)
|
||||
{
|
||||
|
@ -65,7 +67,7 @@ public class OAuthApiController : BaseApiController
|
|||
|
||||
var result = await _authenticationService.AuthenticateSession(request);
|
||||
|
||||
return new JsonResult(new TokenGrantResultModel(result));
|
||||
return new JsonResult(new TokenGrantResponseModel(result));
|
||||
}
|
||||
|
||||
return FailureResult();
|
||||
|
|
53
Astral.ApiServer/Controllers/ServerInfoController.cs
Normal file
53
Astral.ApiServer/Controllers/ServerInfoController.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@
|
|||
// </copyright>
|
||||
|
||||
using Astral.ApiServer.Models;
|
||||
using Astral.ApiServer.Models.Requests;
|
||||
using Astral.ApiServer.Models.Responses;
|
||||
using Astral.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -13,18 +15,24 @@ namespace Astral.ApiServer.Controllers.V1;
|
|||
/// User api controller.
|
||||
/// </summary>
|
||||
[Route("api/v1/user")]
|
||||
[Consumes("application/json")]
|
||||
[Authorize]
|
||||
public class UserApiController : BaseApiController
|
||||
{
|
||||
private readonly IIdentityProvider _identityProvider;
|
||||
private readonly IUserPresenceService _userPresenceService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserApiController"/> class.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
_userPresenceService = userPresenceService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -36,32 +44,90 @@ public class UserApiController : BaseApiController
|
|||
var userId = _identityProvider.GetUserId();
|
||||
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>
|
||||
/// <param name="heartbeatModel">Instance of <see cref="HeartbeatRootRequestModel"/>.</param>
|
||||
[Authorize]
|
||||
[HttpPut("location")]
|
||||
public async Task<IActionResult> ReceiveLocation([FromBody] HeartbeatRootRequestModel heartbeatModel)
|
||||
{
|
||||
if (heartbeatModel?.Location != null)
|
||||
{
|
||||
Success = true,
|
||||
AccountId = userId,
|
||||
Username = userName,
|
||||
XmppPassword = string.Empty,
|
||||
DiscourseApiKey = string.Empty
|
||||
});
|
||||
var heartbeatDto = heartbeatModel.Location.ToUserLocationHeartbeat();
|
||||
await _userPresenceService.ConsumeLocationHeartbeat(heartbeatDto);
|
||||
}
|
||||
|
||||
return SuccessResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does nothing for now since I believe the locker feature is deprecated.
|
||||
/// 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)
|
||||
{
|
||||
return FailureResult();
|
||||
}
|
||||
|
||||
await using (var stream = cert.OpenReadStream())
|
||||
{
|
||||
await _userPresenceService.ConsumePublicKey(stream);
|
||||
}
|
||||
|
||||
return SuccessResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receive the user settings from the client.
|
||||
/// TODO: Investigate this functionality.
|
||||
/// </summary>
|
||||
[Consumes("application/json")]
|
||||
[HttpPost("locker")]
|
||||
public IActionResult PostLocker()
|
||||
public IActionResult PostLockerContents()
|
||||
{
|
||||
return SuccessResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does nothing for now since I believe the locker feature is deprecated.
|
||||
/// Return user settings to the client.
|
||||
/// TODO: Investigate this functionality.
|
||||
/// </summary>
|
||||
[Produces("application/json")]
|
||||
[HttpGet("locker")]
|
||||
public IActionResult GetLocker()
|
||||
public IActionResult GetLockerContents()
|
||||
{
|
||||
return SuccessResult();
|
||||
var req = Request;
|
||||
return SuccessResult(new object());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ public class ExceptionMiddleware
|
|||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = (int)exception.HttpStatusCode;
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResponseModel
|
||||
{
|
||||
Error = exception.ErrorCode,
|
||||
Message = exception.ErrorMessage
|
||||
|
@ -86,7 +86,7 @@ public class ExceptionMiddleware
|
|||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity;
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResponseModel
|
||||
{
|
||||
Error = CoreErrorCodes.ValidationError,
|
||||
Message = exception.Message
|
||||
|
@ -101,7 +101,7 @@ public class ExceptionMiddleware
|
|||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResponseModel
|
||||
{
|
||||
Error = ApiErrorCodes.UnknownError,
|
||||
Message = "Something went wrong. Try again later"
|
||||
|
|
|
@ -14,14 +14,19 @@ namespace Astral.ApiServer.Middleware;
|
|||
public class StatusCodeMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<StatusCodeMiddleware> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StatusCodeMiddleware" /> class.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -42,7 +47,7 @@ public class StatusCodeMiddleware
|
|||
case 401:
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.ContentType = "text/json";
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResponseModel
|
||||
{
|
||||
Error = CoreErrorCodes.Unauthorized,
|
||||
Message = "You're not authorized to do that"
|
||||
|
@ -52,7 +57,8 @@ public class StatusCodeMiddleware
|
|||
case 404:
|
||||
context.Response.Headers.Clear();
|
||||
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,
|
||||
Message = "Unknown method"
|
||||
|
@ -62,7 +68,7 @@ public class StatusCodeMiddleware
|
|||
case 405:
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.ContentType = "text/json";
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResponseModel
|
||||
{
|
||||
Error = ApiErrorCodes.IllegalMethod,
|
||||
Message = "Illegal method"
|
||||
|
@ -72,7 +78,7 @@ public class StatusCodeMiddleware
|
|||
case 415:
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.ContentType = "text/json";
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResponseModel
|
||||
{
|
||||
Error = ApiErrorCodes.UnsupportedBody,
|
||||
Message = "Unsupported body/media type"
|
||||
|
@ -82,7 +88,7 @@ public class StatusCodeMiddleware
|
|||
case 500:
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.ContentType = "text/json";
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResultModel
|
||||
await context.Response.WriteAsJsonAsync(new ErrorResponseModel
|
||||
{
|
||||
Error = ApiErrorCodes.UnknownError,
|
||||
Message = "Unknown error"
|
||||
|
|
30
Astral.ApiServer/Models/Common/DataResponseModel.cs
Normal file
30
Astral.ApiServer/Models/Common/DataResponseModel.cs
Normal 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; }
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
@ -9,7 +9,7 @@ namespace Astral.ApiServer.Models.Common;
|
|||
/// <summary>
|
||||
/// Error result model.
|
||||
/// </summary>
|
||||
public class ErrorResultModel : ResultModel
|
||||
public class ErrorResponseModel : ResponseModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Error code.
|
|
@ -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>
|
||||
|
||||
|
@ -9,8 +9,14 @@ namespace Astral.ApiServer.Models.Common;
|
|||
/// <summary>
|
||||
/// Generic result model.
|
||||
/// </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>
|
||||
/// Indicate success.
|
||||
/// </summary>
|
116
Astral.ApiServer/Models/HeartbeatModel.cs
Normal file
116
Astral.ApiServer/Models/HeartbeatModel.cs
Normal file
|
@ -0,0 +1,116 @@
|
|||
// <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;
|
||||
using Astral.Services.Dtos;
|
||||
|
||||
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 string DomainId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Place id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("place_id")]
|
||||
public string 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 string NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Availability.
|
||||
/// </summary>
|
||||
[JsonPropertyName("availability")]
|
||||
public string Availability { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert this model to <see cref="UserLocationHeartbeatDto"/>.
|
||||
/// </summary>
|
||||
/// <returns>Instance of <see cref="UserLocationHeartbeatDto"/>.</returns>
|
||||
public UserLocationHeartbeatDto ToUserLocationHeartbeat()
|
||||
{
|
||||
var result = new UserLocationHeartbeatDto()
|
||||
{
|
||||
Connected = Connected,
|
||||
Path = Path,
|
||||
NetworkAddress = NetworkAddress,
|
||||
NetworkPort = NetworkPort,
|
||||
Availability = Availability
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(DomainId))
|
||||
{
|
||||
if (Guid.TryParse(DomainId, out var guid))
|
||||
{
|
||||
result.DomainId = guid;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.DomainId = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(PlaceId))
|
||||
{
|
||||
if (Guid.TryParse(PlaceId, out var guid))
|
||||
{
|
||||
result.PlaceId = guid;
|
||||
}
|
||||
else
|
||||
{
|
||||
PlaceId = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(NodeId))
|
||||
{
|
||||
if (Guid.TryParse(NodeId, out var guid))
|
||||
{
|
||||
result.NodeId = guid;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.NodeId = null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
25
Astral.ApiServer/Models/MetaverseVersionModel.cs
Normal file
25
Astral.ApiServer/Models/MetaverseVersionModel.cs
Normal 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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Astral.ApiServer.Models;
|
||||
namespace Astral.ApiServer.Models.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Oauth token grant request.
|
19
Astral.ApiServer/Models/Responses/HeartbeatResponseModel.cs
Normal file
19
Astral.ApiServer/Models/Responses/HeartbeatResponseModel.cs
Normal 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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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>
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Astral.ApiServer.Models.Common;
|
||||
using Astral.Core.Extensions;
|
||||
using Astral.Services.Dtos;
|
||||
|
||||
namespace Astral.ApiServer.Models;
|
||||
namespace Astral.ApiServer.Models.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// OAuth Grant Request Response.
|
||||
/// </summary>
|
||||
public class TokenGrantResultModel : ResultModel
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
public class TokenGrantResponseModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TokenGrantResultModel"/> class.
|
||||
/// </summary>
|
||||
public TokenGrantResultModel()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TokenGrantResultModel"/> class.
|
||||
/// Initializes a new instance of the <see cref="TokenGrantResponseModel"/> class.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
CreatedAt = sessionDto.CreatedAt.Ticks;
|
||||
ExpiresIn = sessionDto.AccessTokenExpires.Ticks;
|
||||
CreatedAt = ((DateTimeOffset)sessionDto.CreatedAt).ToUnixTimeSeconds();
|
||||
ExpiresIn = ((DateTimeOffset)sessionDto.AccessTokenExpires).ToUnixTimeSeconds();
|
||||
RefreshToken = sessionDto.RefreshToken;
|
||||
Scope = sessionDto.Scope.ToString().ToLowerInvariant();
|
||||
AccountId = sessionDto.UserId;
|
|
@ -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; }
|
||||
}
|
|
@ -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>
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Astral.ApiServer.Models.Common;
|
||||
|
||||
namespace Astral.ApiServer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// User profile request result.
|
||||
/// </summary>
|
||||
public class UserProfileResultModel : ResultModel
|
||||
public class UserProfileModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Account id (Even used?).
|
||||
|
@ -35,4 +34,10 @@ public class UserProfileResultModel : ResultModel
|
|||
/// </summary>
|
||||
[JsonPropertyName("xmpp_password")]
|
||||
public string XmppPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Wallet id (Even used?).
|
||||
/// </summary>
|
||||
[JsonPropertyName("wallet_id")]
|
||||
public Guid WalletId { get; set; }
|
||||
}
|
46
Astral.ApiServer/Options/MetaverseInfoOptions.cs
Normal file
46
Astral.ApiServer/Options/MetaverseInfoOptions.cs
Normal 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; }
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
using Astral.ApiServer.Extensions;
|
||||
using Astral.ApiServer.HostedService;
|
||||
using Astral.ApiServer.Middleware;
|
||||
using Astral.ApiServer.Options;
|
||||
using Astral.Core.Options;
|
||||
using Astral.Services.Options;
|
||||
using Astral.Services.Profiles;
|
||||
|
@ -85,6 +86,7 @@ builder.Services.AddAutoMapper(typeof(AutomapperProfile).Assembly);
|
|||
// Setup configuration.
|
||||
builder.Services.Configure<DatabaseOptions>(builder.Configuration.GetSection("Database"));
|
||||
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<RegistrationOptions>(builder.Configuration.GetSection("Registration"));
|
||||
builder.Services.Configure<EmailDomainBlacklistOptions>(
|
||||
|
|
|
@ -16,6 +16,15 @@
|
|||
"SaltSize": 64,
|
||||
"HashSize": 128
|
||||
},
|
||||
"Metaverse": {
|
||||
"Name": "Astral Directory Services",
|
||||
"Nickname": "Astral",
|
||||
"ServerUrl": "http://localhost:5000",
|
||||
"IceServerUrl": "localhost",
|
||||
"DashboardUrl": "localhost",
|
||||
"Version": "0.1 alpha",
|
||||
"Codename": "astral"
|
||||
},
|
||||
"InitialUser": {
|
||||
"Username": "admin",
|
||||
"Email": "admin@changeme.com",
|
||||
|
|
31
Astral.Core/Constants/Discoverability.cs
Normal file
31
Astral.Core/Constants/Discoverability.cs
Normal 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
|
||||
}
|
27
Astral.Core/Entities/UserLocker.cs
Normal file
27
Astral.Core/Entities/UserLocker.cs
Normal 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; }
|
||||
}
|
94
Astral.Core/Entities/UserPresence.cs
Normal file
94
Astral.Core/Entities/UserPresence.cs
Normal 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; }
|
||||
}
|
29
Astral.Core/Extensions/StreamExtensions.cs
Normal file
29
Astral.Core/Extensions/StreamExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
14
Astral.Core/RepositoryInterfaces/IUserLockerRepository.cs
Normal file
14
Astral.Core/RepositoryInterfaces/IUserLockerRepository.cs
Normal 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>
|
||||
{
|
||||
}
|
14
Astral.Core/RepositoryInterfaces/IUserPresenceRepository.cs
Normal file
14
Astral.Core/RepositoryInterfaces/IUserPresenceRepository.cs
Normal 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>
|
||||
{
|
||||
}
|
7
Astral.DAL/Migrations/2024-12-14.02-userLocker.sql
Normal file
7
Astral.DAL/Migrations/2024-12-14.02-userLocker.sql
Normal 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
|
||||
);
|
22
Astral.DAL/Migrations/2024-12-15.01-userPresence.sql
Normal file
22
Astral.DAL/Migrations/2024-12-15.01-userPresence.sql
Normal 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();
|
24
Astral.DAL/Repositories/UserLockerRepository.cs
Normal file
24
Astral.DAL/Repositories/UserLockerRepository.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
24
Astral.DAL/Repositories/UserPresenceRepository.cs
Normal file
24
Astral.DAL/Repositories/UserPresenceRepository.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
51
Astral.Services/Dtos/UserLocationHeartbeatDto.cs
Normal file
51
Astral.Services/Dtos/UserLocationHeartbeatDto.cs
Normal 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; }
|
||||
}
|
37
Astral.Services/Helpers/EntityHelpers.cs
Normal file
37
Astral.Services/Helpers/EntityHelpers.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
// <copyright file="EntityHelpers.cs" company="alveus.dev">
|
||||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
namespace Astral.Services.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Entity helpers.
|
||||
/// </summary>
|
||||
public static class EntityHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply changes only when new entity property is different and not null.
|
||||
/// </summary>
|
||||
/// <param name="oldEntity">Old entity to compare against.</param>
|
||||
/// <param name="newEntity">New entity with new values.</param>
|
||||
/// <typeparam name="TEntity">The type of entity to compare.</typeparam>
|
||||
/// <returns>The new entity with changes applied.</returns>
|
||||
public static TEntity ApplyChanges<TEntity>(TEntity oldEntity, TEntity newEntity)
|
||||
{
|
||||
var entityType = typeof(TEntity);
|
||||
var properties = entityType.GetProperties();
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (property.GetValue(newEntity) != null && property.GetValue(newEntity) != property.GetValue(oldEntity))
|
||||
{
|
||||
property.SetValue(oldEntity, property.GetValue(newEntity));
|
||||
}
|
||||
else
|
||||
{
|
||||
property.SetValue(oldEntity, property.GetValue(oldEntity));
|
||||
}
|
||||
}
|
||||
|
||||
return oldEntity;
|
||||
}
|
||||
}
|
30
Astral.Services/Interfaces/IUserPresenceService.cs
Normal file
30
Astral.Services/Interfaces/IUserPresenceService.cs
Normal 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);
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Core.Constants;
|
||||
using Astral.Core.Entities;
|
||||
using Astral.Services.Dtos;
|
||||
using AutoMapper;
|
||||
|
@ -31,5 +32,20 @@ public class AutomapperProfile : Profile
|
|||
.ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title))
|
||||
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
|
||||
.ForMember(dest => dest.Internal, opt => opt.MapFrom(src => src.Internal));
|
||||
|
||||
CreateMap<UserLocationHeartbeatDto, UserPresence>()
|
||||
.ForMember(
|
||||
dest => dest.Availability,
|
||||
opt => opt.MapFrom((src, dest) =>
|
||||
Enum.TryParse(src.Availability, true, out Discoverability discoverability)
|
||||
? discoverability
|
||||
: Discoverability.None))
|
||||
.ForMember(dest => dest.Connected, opt => opt.MapFrom(src => src.Connected))
|
||||
.ForMember(dest => dest.Path, opt => opt.MapFrom(src => src.Path))
|
||||
.ForMember(dest => dest.DomainId, opt => opt.MapFrom(src => src.DomainId))
|
||||
.ForMember(dest => dest.NetworkAddress, opt => opt.MapFrom(src => src.NetworkAddress))
|
||||
.ForMember(dest => dest.NetworkPort, opt => opt.MapFrom(src => src.NetworkPort))
|
||||
.ForMember(dest => dest.NodeId, opt => opt.MapFrom(src => src.NodeId))
|
||||
.ForMember(dest => dest.PlaceId, opt => opt.MapFrom(src => src.PlaceId));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ using Astral.Services.Exceptions;
|
|||
using Astral.Services.Interfaces;
|
||||
using FluentValidation;
|
||||
using Injectio.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
|
@ -35,6 +36,7 @@ public class AuthenticationService : IAuthenticationService
|
|||
private readonly IRefreshTokenRepository _refreshTokenRepository;
|
||||
private readonly IValidator<PasswordAuthenticateDto> _passwordAuthValidator;
|
||||
private readonly JwtOptions _jwtOptions;
|
||||
private readonly ILogger<AuthenticationService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 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="passwordAuthValidator">Instance of <see cref="IValidator{PasswordAuthenticateDto}"/>.</param>
|
||||
/// <param name="jwtOptions">Instance of <see cref="IOptions{JwtOptions}"/>.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{AuthenticationService}"/>.</param>
|
||||
public AuthenticationService(
|
||||
ICryptographyService cryptographyService,
|
||||
ITransactionProvider transactionProvider,
|
||||
IUserRepository userRepository,
|
||||
IRefreshTokenRepository refreshTokenRepository,
|
||||
IValidator<PasswordAuthenticateDto> passwordAuthValidator,
|
||||
IOptions<JwtOptions> jwtOptions)
|
||||
IOptions<JwtOptions> jwtOptions,
|
||||
ILogger<AuthenticationService> logger)
|
||||
{
|
||||
_cryptographyService = cryptographyService;
|
||||
_transactionProvider = transactionProvider;
|
||||
|
@ -59,6 +63,7 @@ public class AuthenticationService : IAuthenticationService
|
|||
_refreshTokenRepository = refreshTokenRepository;
|
||||
_passwordAuthValidator = passwordAuthValidator;
|
||||
_jwtOptions = jwtOptions.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -102,6 +107,7 @@ public class AuthenticationService : IAuthenticationService
|
|||
// Generate token.
|
||||
var result = await GenerateAccessTokens(user, passwordAuthenticateDto.Scope, passwordAuthenticateDto.IpAddress);
|
||||
|
||||
_logger.LogInformation("Granted authorisation to user {user}", user.Username);
|
||||
transaction.Commit();
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -91,7 +91,8 @@ public class CryptographyService : ICryptographyService
|
|||
rsa.ImportRSAPublicKey(pkcs1Key, out bytesRead);
|
||||
break;
|
||||
}
|
||||
var pem = "";
|
||||
|
||||
var pem = string.Empty;
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
_logger.LogError(
|
||||
|
|
132
Astral.Services/Services/UserPresenceService.cs
Normal file
132
Astral.Services/Services/UserPresenceService.cs
Normal file
|
@ -0,0 +1,132 @@
|
|||
// <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.Helpers;
|
||||
using Astral.Services.Interfaces;
|
||||
using AutoMapper;
|
||||
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;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <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>
|
||||
/// <param name="mapper">Instance of <see cref="IMapper"/>.</param>
|
||||
public UserPresenceService(
|
||||
IIdentityProvider identityProvider,
|
||||
ICryptographyService cryptographyService,
|
||||
IUserPresenceRepository userPresenceRepository,
|
||||
IMapper mapper)
|
||||
{
|
||||
_identityProvider = identityProvider;
|
||||
_cryptographyService = cryptographyService;
|
||||
_userPresenceRepository = userPresenceRepository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
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)
|
||||
{
|
||||
var userId = _identityProvider.GetUserId();
|
||||
|
||||
if (userId == Guid.Empty)
|
||||
{
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
var newHeartbeat = _mapper.Map<UserPresence>(heartbeat);
|
||||
newHeartbeat.Id = userId;
|
||||
|
||||
var oldHeartbeat = await _userPresenceRepository.FindByIdAsync(userId) ?? new UserPresence()
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Id = userId
|
||||
};
|
||||
newHeartbeat.CreatedAt = oldHeartbeat.CreatedAt;
|
||||
newHeartbeat.UpdatedAt = oldHeartbeat.UpdatedAt;
|
||||
newHeartbeat.LastHeartbeat = oldHeartbeat.LastHeartbeat;
|
||||
newHeartbeat = EntityHelpers.ApplyChanges(oldHeartbeat, newHeartbeat);
|
||||
|
||||
await _userPresenceRepository.UpdateAsync(newHeartbeat);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ public class UserService : IUserService
|
|||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserProfileRepository _userProfileRepository;
|
||||
private readonly IUserLockerRepository _userLockerRepository;
|
||||
private readonly ILogger<UserService> _logger;
|
||||
|
||||
/// <summary>
|
||||
|
@ -46,6 +47,7 @@ public class UserService : IUserService
|
|||
/// <param name="transactionProvider">Instance of <see cref="ITransactionProvider" />.</param>
|
||||
/// <param name="userRepository">Instance of <see cref="IUserRepository" />.</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>
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
|
@ -56,6 +58,7 @@ public class UserService : IUserService
|
|||
IOptions<RegistrationOptions> registrationConfig,
|
||||
ITransactionProvider transactionProvider,
|
||||
IUserProfileRepository userProfileRepository,
|
||||
IUserLockerRepository userLockerRepository,
|
||||
ILogger<UserService> logger)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
|
@ -66,6 +69,7 @@ public class UserService : IUserService
|
|||
_registrationConfiguration = registrationConfig.Value;
|
||||
_transactionProvider = transactionProvider;
|
||||
_userProfileRepository = userProfileRepository;
|
||||
_userLockerRepository = userLockerRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue