From 81aa0ec1c01c1a50c47ba2dcab02c62207863196 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 15 Dec 2024 16:06:14 +0000 Subject: [PATCH] WIP heartbeat and user presence --- Alveus.ruleset | 2 +- Astral.ApiServer/Astral.ApiServer.csproj | 1 + .../Controllers/BaseApiController.cs | 28 ++++- .../Controllers/OAuthApiController.cs | 6 +- .../Controllers/ServerInfoController.cs | 53 +++++++++ .../Controllers/V1/UserApiController.cs | 87 ++++++++++++--- .../Middleware/ExceptionMiddleware.cs | 6 +- .../Middleware/StatusCodeMiddleware.cs | 18 ++- .../Models/Common/DataResponseModel.cs | 30 +++++ ...orResultModel.cs => ErrorResponseModel.cs} | 4 +- .../{ResultModel.cs => ResponseModel.cs} | 10 +- Astral.ApiServer/Models/HeartbeatModel.cs | 61 ++++++++++ .../Models/MetaverseVersionModel.cs | 25 +++++ .../Requests/HeartbeatRootRequestModel.cs | 19 ++++ .../{ => Requests}/TokenGrantRequestModel.cs | 2 +- .../Responses/HeartbeatResponseModel.cs | 19 ++++ .../Responses/MetaverseInfoResponseModel.cs | 43 +++++++ .../TokenGrantResponseModel.cs} | 24 ++-- .../Responses/UserProfileResponseModel.cs | 19 ++++ ...fileResultModel.cs => UserProfileModel.cs} | 11 +- .../Options/MetaverseInfoOptions.cs | 46 ++++++++ Astral.ApiServer/Program.cs | 2 + Astral.ApiServer/appsettings.json | 9 ++ Astral.Core/Constants/Discoverability.cs | 31 ++++++ Astral.Core/Entities/UserLocker.cs | 27 +++++ Astral.Core/Entities/UserPresence.cs | 94 ++++++++++++++++ Astral.Core/Extensions/StreamExtensions.cs | 29 +++++ .../IUserLockerRepository.cs | 14 +++ .../IUserPresenceRepository.cs | 14 +++ .../Migrations/2024-12-14.02-userLocker.sql | 7 ++ .../Migrations/2024-12-15.01-userPresence.sql | 22 ++++ .../Repositories/UserLockerRepository.cs | 24 ++++ .../Repositories/UserPresenceRepository.cs | 24 ++++ .../Dtos/UserLocationHeartbeatDto.cs | 51 +++++++++ .../Interfaces/IUserPresenceService.cs | 30 +++++ .../Services/AuthenticationService.cs | 8 +- .../Services/CryptographyService.cs | 3 +- .../Services/UserPresenceService.cs | 105 ++++++++++++++++++ Astral.Services/Services/UserService.cs | 4 + Astral.Tests/GlobalUsings.cs | 6 +- 40 files changed, 959 insertions(+), 59 deletions(-) create mode 100644 Astral.ApiServer/Controllers/ServerInfoController.cs create mode 100644 Astral.ApiServer/Models/Common/DataResponseModel.cs rename Astral.ApiServer/Models/Common/{ErrorResultModel.cs => ErrorResponseModel.cs} (81%) rename Astral.ApiServer/Models/Common/{ResultModel.cs => ResponseModel.cs} (56%) create mode 100644 Astral.ApiServer/Models/HeartbeatModel.cs create mode 100644 Astral.ApiServer/Models/MetaverseVersionModel.cs create mode 100644 Astral.ApiServer/Models/Requests/HeartbeatRootRequestModel.cs rename Astral.ApiServer/Models/{ => Requests}/TokenGrantRequestModel.cs (95%) create mode 100644 Astral.ApiServer/Models/Responses/HeartbeatResponseModel.cs create mode 100644 Astral.ApiServer/Models/Responses/MetaverseInfoResponseModel.cs rename Astral.ApiServer/Models/{TokenGrantResultModel.cs => Responses/TokenGrantResponseModel.cs} (79%) create mode 100644 Astral.ApiServer/Models/Responses/UserProfileResponseModel.cs rename Astral.ApiServer/Models/{UserProfileResultModel.cs => UserProfileModel.cs} (78%) create mode 100644 Astral.ApiServer/Options/MetaverseInfoOptions.cs create mode 100644 Astral.Core/Constants/Discoverability.cs create mode 100644 Astral.Core/Entities/UserLocker.cs create mode 100644 Astral.Core/Entities/UserPresence.cs create mode 100644 Astral.Core/Extensions/StreamExtensions.cs create mode 100644 Astral.Core/RepositoryInterfaces/IUserLockerRepository.cs create mode 100644 Astral.Core/RepositoryInterfaces/IUserPresenceRepository.cs create mode 100644 Astral.DAL/Migrations/2024-12-14.02-userLocker.sql create mode 100644 Astral.DAL/Migrations/2024-12-15.01-userPresence.sql create mode 100644 Astral.DAL/Repositories/UserLockerRepository.cs create mode 100644 Astral.DAL/Repositories/UserPresenceRepository.cs create mode 100644 Astral.Services/Dtos/UserLocationHeartbeatDto.cs create mode 100644 Astral.Services/Interfaces/IUserPresenceService.cs create mode 100644 Astral.Services/Services/UserPresenceService.cs diff --git a/Alveus.ruleset b/Alveus.ruleset index 240d57b..4975031 100644 --- a/Alveus.ruleset +++ b/Alveus.ruleset @@ -2,7 +2,7 @@ - + diff --git a/Astral.ApiServer/Astral.ApiServer.csproj b/Astral.ApiServer/Astral.ApiServer.csproj index 4b02de7..97b7649 100644 --- a/Astral.ApiServer/Astral.ApiServer.csproj +++ b/Astral.ApiServer/Astral.ApiServer.csproj @@ -19,6 +19,7 @@ + diff --git a/Astral.ApiServer/Controllers/BaseApiController.cs b/Astral.ApiServer/Controllers/BaseApiController.cs index 6d9325b..fcf560d 100644 --- a/Astral.ApiServer/Controllers/BaseApiController.cs +++ b/Astral.ApiServer/Controllers/BaseApiController.cs @@ -14,7 +14,7 @@ namespace Astral.ApiServer.Controllers; /// Base API controller with common methods. /// [Produces("application/json")] -[Consumes("application/x-www-form-urlencoded")] +[Consumes("application/json")] public class BaseApiController : ControllerBase { /// @@ -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"); } + /// + /// Return a successful result. + /// + /// Instance of the response model. + /// The response model type. + /// Instance of . + protected IActionResult SuccessResult(TResponseModel response) + where TResponseModel : class + { + Response.StatusCode = (int)HttpStatusCode.OK; + return new JsonResult(new DataResponseModel(response)); + } + /// /// Return a success status with no data. /// @@ -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; diff --git a/Astral.ApiServer/Controllers/OAuthApiController.cs b/Astral.ApiServer/Controllers/OAuthApiController.cs index 77cebbd..6ebf5ba 100644 --- a/Astral.ApiServer/Controllers/OAuthApiController.cs +++ b/Astral.ApiServer/Controllers/OAuthApiController.cs @@ -3,7 +3,8 @@ // 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 /// /// Instance of . [HttpPost("token")] + [Consumes("application/x-www-form-urlencoded")] [AllowAnonymous] public async Task 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(); diff --git a/Astral.ApiServer/Controllers/ServerInfoController.cs b/Astral.ApiServer/Controllers/ServerInfoController.cs new file mode 100644 index 0000000..f814afa --- /dev/null +++ b/Astral.ApiServer/Controllers/ServerInfoController.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +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; + +/// +/// Get information about this instance. +/// +[Route("api")] +public class ServerInfoController : BaseApiController +{ + private readonly MetaverseInfoResponseModel _metaverseInfo; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of . + public ServerInfoController(IOptions 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 + } + }; + } + + /// + /// Get information about this metaverse instance. + /// + /// Instance of . + [HttpGet("metaverse_info")] + [HttpGet("v1/metaverse_info")] + public IActionResult GetMetaverseInformation() + { + return new JsonResult(_metaverseInfo); + } +} diff --git a/Astral.ApiServer/Controllers/V1/UserApiController.cs b/Astral.ApiServer/Controllers/V1/UserApiController.cs index fb4ea2a..9652504 100644 --- a/Astral.ApiServer/Controllers/V1/UserApiController.cs +++ b/Astral.ApiServer/Controllers/V1/UserApiController.cs @@ -3,6 +3,8 @@ // 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. /// [Route("api/v1/user")] +[Consumes("application/json")] [Authorize] public class UserApiController : BaseApiController { private readonly IIdentityProvider _identityProvider; + private readonly IUserPresenceService _userPresenceService; /// /// Initializes a new instance of the class. /// /// Instance of . - public UserApiController(IIdentityProvider identityProvider) + /// Instance of . + public UserApiController( + IIdentityProvider identityProvider, + IUserPresenceService userPresenceService) { _identityProvider = identityProvider; + _userPresenceService = userPresenceService; } /// @@ -36,32 +44,83 @@ 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 + } + }); + } + + /// + /// Receive a user's heartbeat. + /// + [Authorize] + [HttpPut("heartbeat")] + public async Task ReceiveHeartbeat() + { + await _userPresenceService.HandleHeartbeat(); + return SuccessResult(); + } + + /// + /// Receive a user's location heartbeat. + /// + [Authorize] + [HttpPut("location")] + public async Task ReceiveLocation([FromBody] HeartbeatRootRequestModel heartbeatModel) + { + return SuccessResult(); + } + + /// + /// Receive the user's public key. + /// + [HttpPut("public_key")] + [Authorize] + [Consumes("multipart/form-data")] + public async Task PutPublicKey() + { + var cert = HttpContext.Request.Form.Files.GetFile("public_key"); + + if (cert is null) { - Success = true, - AccountId = userId, - Username = userName, - XmppPassword = string.Empty, - DiscourseApiKey = string.Empty - }); + return FailureResult(); + } + + await using (var stream = cert.OpenReadStream()) + { + await _userPresenceService.ConsumePublicKey(stream); + } + + return SuccessResult(); } /// - /// Does nothing for now since I believe the locker feature is deprecated. + /// Receive the user settings from the client. + /// TODO: Investigate this functionality. /// + [Consumes("application/json")] [HttpPost("locker")] - public IActionResult PostLocker() + public IActionResult PostLockerContents() { return SuccessResult(); } /// - /// Does nothing for now since I believe the locker feature is deprecated. + /// Return user settings to the client. + /// TODO: Investigate this functionality. /// + [Produces("application/json")] [HttpGet("locker")] - public IActionResult GetLocker() + public IActionResult GetLockerContents() { - return SuccessResult(); + var req = Request; + return SuccessResult(new object()); } - } diff --git a/Astral.ApiServer/Middleware/ExceptionMiddleware.cs b/Astral.ApiServer/Middleware/ExceptionMiddleware.cs index 4d35d23..4827f2a 100644 --- a/Astral.ApiServer/Middleware/ExceptionMiddleware.cs +++ b/Astral.ApiServer/Middleware/ExceptionMiddleware.cs @@ -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" diff --git a/Astral.ApiServer/Middleware/StatusCodeMiddleware.cs b/Astral.ApiServer/Middleware/StatusCodeMiddleware.cs index 1782f47..add0798 100644 --- a/Astral.ApiServer/Middleware/StatusCodeMiddleware.cs +++ b/Astral.ApiServer/Middleware/StatusCodeMiddleware.cs @@ -14,14 +14,19 @@ namespace Astral.ApiServer.Middleware; public class StatusCodeMiddleware { private readonly RequestDelegate _next; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// Instance of . - public StatusCodeMiddleware(RequestDelegate next) + /// Instance of . + public StatusCodeMiddleware( + RequestDelegate next, + ILogger logger) { _next = next; + _logger = logger; } /// @@ -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" diff --git a/Astral.ApiServer/Models/Common/DataResponseModel.cs b/Astral.ApiServer/Models/Common/DataResponseModel.cs new file mode 100644 index 0000000..56982ff --- /dev/null +++ b/Astral.ApiServer/Models/Common/DataResponseModel.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using System.Text.Json.Serialization; + +namespace Astral.ApiServer.Models.Common; + +/// +/// Success with data response. +/// +public class DataResponseModel : ResponseModel +{ + /// + /// Initializes a new instance of the class. + /// + /// Instance of . + public DataResponseModel(TDataType data) + { + Status = "success"; + Success = true; + Data = data; + } + + /// + /// The data response. + /// + [JsonPropertyName("data")] + public TDataType Data { get; set; } +} diff --git a/Astral.ApiServer/Models/Common/ErrorResultModel.cs b/Astral.ApiServer/Models/Common/ErrorResponseModel.cs similarity index 81% rename from Astral.ApiServer/Models/Common/ErrorResultModel.cs rename to Astral.ApiServer/Models/Common/ErrorResponseModel.cs index 23f3a30..c987a73 100644 --- a/Astral.ApiServer/Models/Common/ErrorResultModel.cs +++ b/Astral.ApiServer/Models/Common/ErrorResponseModel.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // @@ -9,7 +9,7 @@ namespace Astral.ApiServer.Models.Common; /// /// Error result model. /// -public class ErrorResultModel : ResultModel +public class ErrorResponseModel : ResponseModel { /// /// Error code. diff --git a/Astral.ApiServer/Models/Common/ResultModel.cs b/Astral.ApiServer/Models/Common/ResponseModel.cs similarity index 56% rename from Astral.ApiServer/Models/Common/ResultModel.cs rename to Astral.ApiServer/Models/Common/ResponseModel.cs index 6f0fca8..28f9c13 100644 --- a/Astral.ApiServer/Models/Common/ResultModel.cs +++ b/Astral.ApiServer/Models/Common/ResponseModel.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // @@ -9,8 +9,14 @@ namespace Astral.ApiServer.Models.Common; /// /// Generic result model. /// -public class ResultModel +public class ResponseModel { + /// + /// Interface requires a string to represent success or failure rather than a boolean. + /// + [JsonPropertyName("status")] + public string Status { get; set; } + /// /// Indicate success. /// diff --git a/Astral.ApiServer/Models/HeartbeatModel.cs b/Astral.ApiServer/Models/HeartbeatModel.cs new file mode 100644 index 0000000..f2a3c21 --- /dev/null +++ b/Astral.ApiServer/Models/HeartbeatModel.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using System.Text.Json.Serialization; + +namespace Astral.ApiServer.Models; + +/// +/// Heartbeat contents. +/// +public class HeartbeatModel +{ + /// + /// True if connected. + /// + [JsonPropertyName("connected")] + public bool Connected { get; set; } + + /// + /// Path. + /// + [JsonPropertyName("path")] + public string Path { get; set; } + + /// + /// Domain id. + /// + [JsonPropertyName("domain_id")] + public Guid DomainId { get; set; } + + /// + /// Place id. + /// + [JsonPropertyName("place_id")] + public Guid PlaceId { get; set; } + + /// + /// Network address. + /// + [JsonPropertyName("network_address")] + public string NetworkAddress { get; set; } + + /// + /// Network port. + /// + [JsonPropertyName("network_port")] + public int NetworkPort { get; set; } + + /// + /// Node id. + /// + [JsonPropertyName("node_id")] + public Guid NodeId { get; set; } + + /// + /// Availability. + /// + [JsonPropertyName("availability")] + public string Availability { get; set; } +} diff --git a/Astral.ApiServer/Models/MetaverseVersionModel.cs b/Astral.ApiServer/Models/MetaverseVersionModel.cs new file mode 100644 index 0000000..7ce1f39 --- /dev/null +++ b/Astral.ApiServer/Models/MetaverseVersionModel.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using System.Text.Json.Serialization; + +namespace Astral.ApiServer.Models; + +/// +/// Version information about this instance. +/// +public class MetaverseVersionModel +{ + /// + /// Version string. + /// + [JsonPropertyName("version")] + public string Version { get; set; } + + /// + /// Codename. + /// + [JsonPropertyName("codename")] + public string Codename { get; set; } +} diff --git a/Astral.ApiServer/Models/Requests/HeartbeatRootRequestModel.cs b/Astral.ApiServer/Models/Requests/HeartbeatRootRequestModel.cs new file mode 100644 index 0000000..285a953 --- /dev/null +++ b/Astral.ApiServer/Models/Requests/HeartbeatRootRequestModel.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using System.Text.Json.Serialization; + +namespace Astral.ApiServer.Models.Requests; + +/// +/// Heartbeat request model. +/// +public class HeartbeatRootRequestModel +{ + /// + /// Heartbeat location information. + /// + [JsonPropertyName("location")] + public HeartbeatModel Location { get; set; } +} diff --git a/Astral.ApiServer/Models/TokenGrantRequestModel.cs b/Astral.ApiServer/Models/Requests/TokenGrantRequestModel.cs similarity index 95% rename from Astral.ApiServer/Models/TokenGrantRequestModel.cs rename to Astral.ApiServer/Models/Requests/TokenGrantRequestModel.cs index 9c9104d..a48ee05 100644 --- a/Astral.ApiServer/Models/TokenGrantRequestModel.cs +++ b/Astral.ApiServer/Models/Requests/TokenGrantRequestModel.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc; -namespace Astral.ApiServer.Models; +namespace Astral.ApiServer.Models.Requests; /// /// Oauth token grant request. diff --git a/Astral.ApiServer/Models/Responses/HeartbeatResponseModel.cs b/Astral.ApiServer/Models/Responses/HeartbeatResponseModel.cs new file mode 100644 index 0000000..ec718a1 --- /dev/null +++ b/Astral.ApiServer/Models/Responses/HeartbeatResponseModel.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using System.Text.Json.Serialization; + +namespace Astral.ApiServer.Models.Responses; + +/// +/// The response given from a heartbeat request. +/// +public class HeartbeatResponseModel +{ + /// + /// Session id. + /// + [JsonPropertyName("session_id")] + public Guid SessionId { get; set; } +} diff --git a/Astral.ApiServer/Models/Responses/MetaverseInfoResponseModel.cs b/Astral.ApiServer/Models/Responses/MetaverseInfoResponseModel.cs new file mode 100644 index 0000000..16bcb03 --- /dev/null +++ b/Astral.ApiServer/Models/Responses/MetaverseInfoResponseModel.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using System.Text.Json.Serialization; + +namespace Astral.ApiServer.Models.Responses; + +/// +/// Information about this instance. +/// +public class MetaverseInfoResponseModel +{ + /// + /// Metaverse name. + /// + [JsonPropertyName("metaverse_name")] + public string MetaverseName { get; set; } + + /// + /// Metaverse nickname. + /// + [JsonPropertyName("metaverse_nick_name")] + public string MetaverseNickName { get; set; } + + /// + /// Url of this server. + /// + [JsonPropertyName("metaverse_url")] + public string MetaverseUrl { get; set; } + + /// + /// ICE server url. + /// + [JsonPropertyName("ice_server_url")] + public string IceServerUrl { get; set; } + + /// + /// Version information. + /// + [JsonPropertyName("metaverse_server_version")] + public MetaverseVersionModel Version { get; set; } +} diff --git a/Astral.ApiServer/Models/TokenGrantResultModel.cs b/Astral.ApiServer/Models/Responses/TokenGrantResponseModel.cs similarity index 79% rename from Astral.ApiServer/Models/TokenGrantResultModel.cs rename to Astral.ApiServer/Models/Responses/TokenGrantResponseModel.cs index ce492b8..520ebc5 100644 --- a/Astral.ApiServer/Models/TokenGrantResultModel.cs +++ b/Astral.ApiServer/Models/Responses/TokenGrantResponseModel.cs @@ -1,36 +1,28 @@ -// +// // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // 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; /// /// OAuth Grant Request Response. /// -public class TokenGrantResultModel : ResultModel +// ReSharper disable UnusedAutoPropertyAccessor.Global +public class TokenGrantResponseModel { /// - /// Initializes a new instance of the class. - /// - public TokenGrantResultModel() - { - } - - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Instance of to create from. - 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; diff --git a/Astral.ApiServer/Models/Responses/UserProfileResponseModel.cs b/Astral.ApiServer/Models/Responses/UserProfileResponseModel.cs new file mode 100644 index 0000000..b4831f0 --- /dev/null +++ b/Astral.ApiServer/Models/Responses/UserProfileResponseModel.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using System.Text.Json.Serialization; + +namespace Astral.ApiServer.Models.Responses; + +/// +/// User response model. +/// +public class UserProfileResponseModel +{ + /// + /// User profile. + /// + [JsonPropertyName("user")] + public UserProfileModel User { get; set; } +} diff --git a/Astral.ApiServer/Models/UserProfileResultModel.cs b/Astral.ApiServer/Models/UserProfileModel.cs similarity index 78% rename from Astral.ApiServer/Models/UserProfileResultModel.cs rename to Astral.ApiServer/Models/UserProfileModel.cs index f6937c2..d896514 100644 --- a/Astral.ApiServer/Models/UserProfileResultModel.cs +++ b/Astral.ApiServer/Models/UserProfileModel.cs @@ -1,16 +1,15 @@ -// +// // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // using System.Text.Json.Serialization; -using Astral.ApiServer.Models.Common; namespace Astral.ApiServer.Models; /// /// User profile request result. /// -public class UserProfileResultModel : ResultModel +public class UserProfileModel { /// /// Account id (Even used?). @@ -35,4 +34,10 @@ public class UserProfileResultModel : ResultModel /// [JsonPropertyName("xmpp_password")] public string XmppPassword { get; set; } + + /// + /// Wallet id (Even used?). + /// + [JsonPropertyName("wallet_id")] + public Guid WalletId { get; set; } } diff --git a/Astral.ApiServer/Options/MetaverseInfoOptions.cs b/Astral.ApiServer/Options/MetaverseInfoOptions.cs new file mode 100644 index 0000000..2568ca5 --- /dev/null +++ b/Astral.ApiServer/Options/MetaverseInfoOptions.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +namespace Astral.ApiServer.Options; + +/// +/// Metaverse information options. +/// +public class MetaverseInfoOptions +{ + /// + /// Metaverse name. + /// + public string Name { get; set; } + + /// + /// Metaverse nickname. + /// + public string Nickname { get; set; } + + /// + /// Metaverse server url. + /// + public string ServerUrl { get; set; } + + /// + /// Ice server url. + /// + public string IceServerUrl { get; set; } + + /// + /// Dashboard url. + /// + public string DashboardUrl { get; set; } + + /// + /// Metaverse version. + /// + public string Version { get; set; } + + /// + /// Metaverse codename. + /// + public string Codename { get; set; } +} diff --git a/Astral.ApiServer/Program.cs b/Astral.ApiServer/Program.cs index c92f5ab..311677f 100644 --- a/Astral.ApiServer/Program.cs +++ b/Astral.ApiServer/Program.cs @@ -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(builder.Configuration.GetSection("Database")); builder.Services.Configure(builder.Configuration.GetSection("PwdHash")); +builder.Services.Configure(builder.Configuration.GetSection("Metaverse")); builder.Services.Configure(builder.Configuration.GetSection("InitialUser")); builder.Services.Configure(builder.Configuration.GetSection("Registration")); builder.Services.Configure( diff --git a/Astral.ApiServer/appsettings.json b/Astral.ApiServer/appsettings.json index cfd71b8..a248f48 100644 --- a/Astral.ApiServer/appsettings.json +++ b/Astral.ApiServer/appsettings.json @@ -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", diff --git a/Astral.Core/Constants/Discoverability.cs b/Astral.Core/Constants/Discoverability.cs new file mode 100644 index 0000000..3307c51 --- /dev/null +++ b/Astral.Core/Constants/Discoverability.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +namespace Astral.Core.Constants; + +/// +/// User's discoverability/availability. +/// +public enum Discoverability +{ + /// + /// Discoverable to none. + /// + None, + + /// + /// Discoverable only to friends. + /// + Friends, + + /// + /// Discoverable to connections. + /// + Connections, + + /// + /// Discoverable to all. + /// + All +} diff --git a/Astral.Core/Entities/UserLocker.cs b/Astral.Core/Entities/UserLocker.cs new file mode 100644 index 0000000..3146ddd --- /dev/null +++ b/Astral.Core/Entities/UserLocker.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using Astral.Core.Attributes.EntityAnnotation; + +namespace Astral.Core.Entities; + +/// +/// User locker entity. +/// +[TableMapping("userLocker")] +public class UserLocker +{ + /// + /// User id. + /// + [ColumnMapping("id")] + [PrimaryKey] + public Guid Id { get; set; } + + /// + /// Locker contents. + /// + [ColumnMapping("contents")] + public string Contents { get; set; } +} diff --git a/Astral.Core/Entities/UserPresence.cs b/Astral.Core/Entities/UserPresence.cs new file mode 100644 index 0000000..cdddadb --- /dev/null +++ b/Astral.Core/Entities/UserPresence.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using Astral.Core.Attributes.EntityAnnotation; +using Astral.Core.Constants; + +namespace Astral.Core.Entities; + +/// +/// User presence entity. +/// +[TableMapping("userPresence")] +public class UserPresence +{ + /// + /// The user id this belongs to. + /// + [PrimaryKey] + [ColumnMapping("id")] + public Guid Id { get; set; } + + /// + /// When the entity was created. + /// + [ColumnMapping("createdAt")] + public DateTime CreatedAt { get; set; } + + /// + /// When the entity was last updated. + /// + [ColumnMapping("updatedAt")] + public DateTime UpdatedAt { get; set; } + + /// + /// True if connected. + /// + [ColumnMapping("connected")] + public bool? Connected { get; set; } + + /// + /// Current domain id. + /// + [ColumnMapping("domainId")] + public Guid? DomainId { get; set; } + + /// + /// Current place id. + /// + [ColumnMapping("placeId")] + public Guid? PlaceId { get; set; } + + /// + /// Current network address. + /// + [ColumnMapping("networkAddress")] + public string NetworkAddress { get; set; } + + /// + /// Current network port. + /// + [ColumnMapping("networkPort")] + public int? NetworkPort { get; set; } + + /// + /// Current public key. + /// + [ColumnMapping("publicKey")] + public string PublicKey { get; set; } + + /// + /// Current path. + /// + [ColumnMapping("path")] + public string Path { get; set; } + + /// + /// Last heartbeat received. + /// + [ColumnMapping("lastHeartbeat")] + public DateTime LastHeartbeat { get; set; } + + /// + /// Node id. + /// + [ColumnMapping("nodeId")] + public Guid? NodeId { get; set; } + + /// + /// Discoverability. + /// + [ColumnMapping("availability")] + public Discoverability Availability { get; set; } +} diff --git a/Astral.Core/Extensions/StreamExtensions.cs b/Astral.Core/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..c2bdf54 --- /dev/null +++ b/Astral.Core/Extensions/StreamExtensions.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +namespace Astral.Core.Extensions; + +/// +/// extensions. +/// +public static class StreamExtensions +{ + /// + /// Return a byte array of the contents of the stream. + /// + /// Instance of . + /// Collection of bytes. + 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; + } +} diff --git a/Astral.Core/RepositoryInterfaces/IUserLockerRepository.cs b/Astral.Core/RepositoryInterfaces/IUserLockerRepository.cs new file mode 100644 index 0000000..4b35513 --- /dev/null +++ b/Astral.Core/RepositoryInterfaces/IUserLockerRepository.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using Astral.Core.Entities; + +namespace Astral.Core.RepositoryInterfaces; + +/// +/// Repository. +/// +public interface IUserLockerRepository : IGenericRepository +{ +} diff --git a/Astral.Core/RepositoryInterfaces/IUserPresenceRepository.cs b/Astral.Core/RepositoryInterfaces/IUserPresenceRepository.cs new file mode 100644 index 0000000..5b0e1aa --- /dev/null +++ b/Astral.Core/RepositoryInterfaces/IUserPresenceRepository.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using Astral.Core.Entities; + +namespace Astral.Core.RepositoryInterfaces; + +/// +/// repository. +/// +public interface IUserPresenceRepository : IGenericRepository +{ +} diff --git a/Astral.DAL/Migrations/2024-12-14.02-userLocker.sql b/Astral.DAL/Migrations/2024-12-14.02-userLocker.sql new file mode 100644 index 0000000..903fa1f --- /dev/null +++ b/Astral.DAL/Migrations/2024-12-14.02-userLocker.sql @@ -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 +); diff --git a/Astral.DAL/Migrations/2024-12-15.01-userPresence.sql b/Astral.DAL/Migrations/2024-12-15.01-userPresence.sql new file mode 100644 index 0000000..dbbabed --- /dev/null +++ b/Astral.DAL/Migrations/2024-12-15.01-userPresence.sql @@ -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(); diff --git a/Astral.DAL/Repositories/UserLockerRepository.cs b/Astral.DAL/Repositories/UserLockerRepository.cs new file mode 100644 index 0000000..7446999 --- /dev/null +++ b/Astral.DAL/Repositories/UserLockerRepository.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using Astral.Core.Entities; +using Astral.Core.Infrastructure; +using Astral.Core.RepositoryInterfaces; +using Injectio.Attributes; + +namespace Astral.DAL.Repositories; + +/// +[RegisterScoped] +public class UserLockerRepository : BaseRepository, IUserLockerRepository +{ + /// + /// Initializes a new instance of the class. + /// + /// Instance of . + public UserLockerRepository(IDbConnectionProvider db) + : base(db) + { + } +} diff --git a/Astral.DAL/Repositories/UserPresenceRepository.cs b/Astral.DAL/Repositories/UserPresenceRepository.cs new file mode 100644 index 0000000..5a9b6fe --- /dev/null +++ b/Astral.DAL/Repositories/UserPresenceRepository.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using Astral.Core.Entities; +using Astral.Core.Infrastructure; +using Astral.Core.RepositoryInterfaces; +using Injectio.Attributes; + +namespace Astral.DAL.Repositories; + +/// +[RegisterScoped] +public class UserPresenceRepository : BaseRepository, IUserPresenceRepository +{ + /// + /// Initializes a new instance of the class. + /// + /// Instance of . + public UserPresenceRepository(IDbConnectionProvider db) + : base(db) + { + } +} diff --git a/Astral.Services/Dtos/UserLocationHeartbeatDto.cs b/Astral.Services/Dtos/UserLocationHeartbeatDto.cs new file mode 100644 index 0000000..3154dda --- /dev/null +++ b/Astral.Services/Dtos/UserLocationHeartbeatDto.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +namespace Astral.Services.Dtos; + +/// +/// Heartbeat containing location data. +/// +public class UserLocationHeartbeatDto +{ + /// + /// True if connected. + /// + public bool Connected { get; set; } + + /// + /// Path. + /// + public string Path { get; set; } + + /// + /// Domain id. + /// + public Guid DomainId { get; set; } + + /// + /// Place id. + /// + public Guid PlaceId { get; set; } + + /// + /// Network address. + /// + public string NetworkAddress { get; set; } + + /// + /// Network port. + /// + public int NetworkPort { get; set; } + + /// + /// Node id. + /// + public Guid NodeId { get; set; } + + /// + /// Availability. + /// + public string Availability { get; set; } +} diff --git a/Astral.Services/Interfaces/IUserPresenceService.cs b/Astral.Services/Interfaces/IUserPresenceService.cs new file mode 100644 index 0000000..3079ac7 --- /dev/null +++ b/Astral.Services/Interfaces/IUserPresenceService.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +using Astral.Services.Dtos; + +namespace Astral.Services.Interfaces; + +/// +/// User's presence service. +/// +public interface IUserPresenceService +{ + /// + /// Process a user's heartbeat. + /// + Task HandleHeartbeat(); + + /// + /// Process a user's heartbeat with location. + /// + /// Instance of . + Task ConsumeLocationHeartbeat(UserLocationHeartbeatDto heartbeat); + + /// + /// Update the user's public key. + /// + /// Stream containing the public key. + Task ConsumePublicKey(Stream publicKey); +} diff --git a/Astral.Services/Services/AuthenticationService.cs b/Astral.Services/Services/AuthenticationService.cs index f9546d9..a4e6228 100644 --- a/Astral.Services/Services/AuthenticationService.cs +++ b/Astral.Services/Services/AuthenticationService.cs @@ -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 _passwordAuthValidator; private readonly JwtOptions _jwtOptions; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -45,13 +47,15 @@ public class AuthenticationService : IAuthenticationService /// Instance of . /// Instance of . /// Instance of . + /// Instance of . public AuthenticationService( ICryptographyService cryptographyService, ITransactionProvider transactionProvider, IUserRepository userRepository, IRefreshTokenRepository refreshTokenRepository, IValidator passwordAuthValidator, - IOptions jwtOptions) + IOptions jwtOptions, + ILogger logger) { _cryptographyService = cryptographyService; _transactionProvider = transactionProvider; @@ -59,6 +63,7 @@ public class AuthenticationService : IAuthenticationService _refreshTokenRepository = refreshTokenRepository; _passwordAuthValidator = passwordAuthValidator; _jwtOptions = jwtOptions.Value; + _logger = logger; } /// @@ -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; } diff --git a/Astral.Services/Services/CryptographyService.cs b/Astral.Services/Services/CryptographyService.cs index 323e6a9..4c71f39 100644 --- a/Astral.Services/Services/CryptographyService.cs +++ b/Astral.Services/Services/CryptographyService.cs @@ -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( diff --git a/Astral.Services/Services/UserPresenceService.cs b/Astral.Services/Services/UserPresenceService.cs new file mode 100644 index 0000000..a53fb05 --- /dev/null +++ b/Astral.Services/Services/UserPresenceService.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +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; + +/// +[RegisterScoped] +public class UserPresenceService : IUserPresenceService +{ + private readonly IIdentityProvider _identityProvider; + private readonly ICryptographyService _cryptographyService; + private readonly IUserPresenceRepository _userPresenceRepository; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of . + /// Instance of . + /// Instance of . + public UserPresenceService( + IIdentityProvider identityProvider, + ICryptographyService cryptographyService, + IUserPresenceRepository userPresenceRepository) + { + _identityProvider = identityProvider; + _cryptographyService = cryptographyService; + _userPresenceRepository = userPresenceRepository; + } + + /// + 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); + } + } + + /// + public async Task ConsumeLocationHeartbeat(UserLocationHeartbeatDto heartbeat) + { + throw new NotImplementedException(); + } + + /// + 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); + } + } +} diff --git a/Astral.Services/Services/UserService.cs b/Astral.Services/Services/UserService.cs index 3d0b0ba..1b3c8b6 100644 --- a/Astral.Services/Services/UserService.cs +++ b/Astral.Services/Services/UserService.cs @@ -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 _logger; /// @@ -46,6 +47,7 @@ public class UserService : IUserService /// Instance of . /// Instance of . /// Instance of . + /// Instance of . /// Instance of . public UserService( IUserRepository userRepository, @@ -56,6 +58,7 @@ public class UserService : IUserService IOptions registrationConfig, ITransactionProvider transactionProvider, IUserProfileRepository userProfileRepository, + IUserLockerRepository userLockerRepository, ILogger logger) { _userRepository = userRepository; @@ -66,6 +69,7 @@ public class UserService : IUserService _registrationConfiguration = registrationConfig.Value; _transactionProvider = transactionProvider; _userProfileRepository = userProfileRepository; + _userLockerRepository = userLockerRepository; _logger = logger; } diff --git a/Astral.Tests/GlobalUsings.cs b/Astral.Tests/GlobalUsings.cs index 8c927eb..e2d751e 100644 --- a/Astral.Tests/GlobalUsings.cs +++ b/Astral.Tests/GlobalUsings.cs @@ -1 +1,5 @@ -global using Xunit; \ No newline at end of file +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +global using Xunit;