diff --git a/API.IntegrationTests/Tests/AccountAuthenticatedTests.cs b/API.IntegrationTests/Tests/AccountAuthenticatedTests.cs index 04a66b76..0d19893b 100644 --- a/API.IntegrationTests/Tests/AccountAuthenticatedTests.cs +++ b/API.IntegrationTests/Tests/AccountAuthenticatedTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Text.Json; using OpenShock.API.IntegrationTests.Helpers; namespace OpenShock.API.IntegrationTests.Tests; @@ -8,6 +9,44 @@ public sealed class AccountAuthenticatedTests [ClassDataSource(Shared = SharedType.PerTestSession)] public required WebApplicationFactory WebApplicationFactory { get; init; } + // --- Get Self --- + + [Test] + public async Task GetSelf_ReturnsCurrentUserInfo_WithoutLegacyWrapper() + { + var user = await TestHelper.CreateAndLoginUser(WebApplicationFactory, "accself", "accself@test.org", "SecurePassword123#"); + using var client = TestHelper.CreateAuthenticatedClient(WebApplicationFactory, user.SessionToken); + + var response = await client.GetAsync("/1/account"); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + var root = doc.RootElement; + await Assert.That(root.TryGetProperty("data", out _)).IsFalse(); + await Assert.That(root.GetProperty("name").GetString()).IsEqualTo("accself"); + await Assert.That(root.GetProperty("email").GetString()).IsEqualTo("accself@test.org"); + await Assert.That(root.GetProperty("hasPassword").GetBoolean()).IsTrue(); + await Assert.That(root.TryGetProperty("id", out _)).IsTrue(); + await Assert.That(root.TryGetProperty("image", out _)).IsTrue(); + await Assert.That(root.TryGetProperty("roles", out _)).IsTrue(); + } + + [Test] + public async Task GetSelf_WithApiToken_ReturnsUserInfo() + { + var userId = await TestHelper.CreateUserInDb(WebApplicationFactory, "accselfapi", "accselfapi@test.org", "SecurePassword123#"); + var (_, rawToken) = await TestHelper.CreateApiTokenInDb(WebApplicationFactory, userId, "AccountSelfApiToken"); + using var client = TestHelper.CreateApiTokenClient(WebApplicationFactory, rawToken); + + var response = await client.GetAsync("/1/account"); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + await Assert.That(doc.RootElement.GetProperty("name").GetString()).IsEqualTo("accselfapi"); + } + // --- Change Password --- [Test] diff --git a/API.IntegrationTests/Tests/AuthorizationTests.cs b/API.IntegrationTests/Tests/AuthorizationTests.cs index 1dfcd161..c3785ba9 100644 --- a/API.IntegrationTests/Tests/AuthorizationTests.cs +++ b/API.IntegrationTests/Tests/AuthorizationTests.cs @@ -25,6 +25,7 @@ public sealed class AuthorizationTests [Arguments("/1/tokens")] [Arguments("/1/sessions/self")] [Arguments("/1/users/self")] + [Arguments("/1/account")] public async Task ProtectedEndpoint_NoAuth_Returns401(string url) { using var client = WebApplicationFactory.CreateClient(); diff --git a/API/Controller/Account/Authenticated/GetSelf.cs b/API/Controller/Account/Authenticated/GetSelf.cs new file mode 100644 index 00000000..77492498 --- /dev/null +++ b/API/Controller/Account/Authenticated/GetSelf.cs @@ -0,0 +1,29 @@ +using System.Net.Mime; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OpenShock.API.Models.Response; +using OpenShock.Common.Authentication; +using OpenShock.Common.Extensions; +using OpenShock.Common.OpenShockDb; + +namespace OpenShock.API.Controller.Account.Authenticated; + +public sealed partial class AuthenticatedAccountController +{ + /// + /// Get the current user's account information. + /// + /// The account information was successfully retrieved. + [HttpGet] + [Authorize(AuthenticationSchemes = OpenShockAuthSchemes.UserSessionApiTokenCombo)] + [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + public UserSelfResponse GetSelf() => new() + { + Id = CurrentUser.Id, + Name = CurrentUser.Name, + Email = CurrentUser.Email, + Image = CurrentUser.GetImageUrl(), + Roles = CurrentUser.Roles, + HasPassword = !string.IsNullOrEmpty(CurrentUser.PasswordHash) + }; +} diff --git a/API/Controller/Users/GetSelf.cs b/API/Controller/Users/GetSelf.cs index b3a3ced8..fcfaa747 100644 --- a/API/Controller/Users/GetSelf.cs +++ b/API/Controller/Users/GetSelf.cs @@ -1,23 +1,24 @@ using System.Net.Mime; using Microsoft.AspNetCore.Mvc; +using OpenShock.API.Models.Response; using OpenShock.Common.Extensions; using OpenShock.Common.Models; -using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Controller.Users; public sealed partial class UsersController { /// - /// Get the current user's information. + /// Get the current user's information. Deprecated: use GET /1/account instead. /// /// The user's information was successfully retrieved. + [Obsolete("Use GET /1/account instead.")] [HttpGet("self")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public IActionResult GetSelf() { return LegacyDataOk( - new UserSelfResponse + new LegacyUserSelfResponse { Id = CurrentUser.Id, Name = CurrentUser.Name, @@ -29,15 +30,4 @@ public IActionResult GetSelf() } ); } - - public sealed class UserSelfResponse - { - public required Guid Id { get; init; } - public required string Name { get; init; } - public required string Email { get; init; } - public required Uri Image { get; init; } - public required List Roles { get; init; } - public required string Rank { get; init; } - public required bool HasPassword { get; init; } - } -} \ No newline at end of file +} diff --git a/API/Models/Response/LegacyUserSelfResponse.cs b/API/Models/Response/LegacyUserSelfResponse.cs new file mode 100644 index 00000000..fcfc1fdd --- /dev/null +++ b/API/Models/Response/LegacyUserSelfResponse.cs @@ -0,0 +1,17 @@ +using OpenShock.Common.OpenShockDb; + +namespace OpenShock.API.Models.Response; + +/// +/// Response shape of the deprecated GET /1/users/self, kept for API token clients that still read rank. +/// +public sealed class LegacyUserSelfResponse +{ + public required Guid Id { get; init; } + public required string Name { get; init; } + public required string Email { get; init; } + public required Uri Image { get; init; } + public required List Roles { get; init; } + public required string Rank { get; init; } + public required bool HasPassword { get; init; } +} diff --git a/API/Models/Response/UserSelfResponse.cs b/API/Models/Response/UserSelfResponse.cs new file mode 100644 index 00000000..cf1bac66 --- /dev/null +++ b/API/Models/Response/UserSelfResponse.cs @@ -0,0 +1,13 @@ +using OpenShock.Common.OpenShockDb; + +namespace OpenShock.API.Models.Response; + +public sealed class UserSelfResponse +{ + public required Guid Id { get; init; } + public required string Name { get; init; } + public required string Email { get; init; } + public required Uri Image { get; init; } + public required List Roles { get; init; } + public required bool HasPassword { get; init; } +}