feat: Add fully user functions
This commit is contained in:
parent
6e156ee3a4
commit
271165fb88
@ -6,6 +6,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticatorTokenProvider_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd56cb0a089b14dab96ad3ee133819f966d938_003Feb_003Fa2d5eee1_003FAuthenticatorTokenProvider_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaimsPrincipal_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf5ce3f8ae0647439d514bb1a3c7f96b13600_003F20_003Fabeaf9ae_003FClaimsPrincipal_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaimTypes_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf5ce3f8ae0647439d514bb1a3c7f96b13600_003Fbd_003F4cde67a5_003FClaimTypes_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdffdaf205cf54e098aa7d66ba76b38621de920_003F53_003F6f15feba_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003Fbc_003F2b4c89d0_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityDbContext_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3eeae7a548684642a53a9ceddc825b7a1a930_003Fcf_003F6a374370_003FIdentityDbContext_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUserToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Facfead36ed0138084f13ee724545d2dcb853f354ec658443b72fc26eff58781_003FIdentityUserToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@ -18,15 +19,16 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASignInManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F40411c547364428dafc988a7615774e28b910_003F6d_003F1a409232_003FSignInManager_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee39d1e9346e41aa9d44f0e1b1c6630f76268_003F49_003Fb92346b2_003FStringValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATestMethodInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5ef53d675c5d34a6b85963919015dc0c1b06e5ea9834aac59ae6911f4c6f38_003FTestMethodInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb3abda585dc54c3f81f64fdda8fc5ba72b708_003F2f_003F1d20f21a_003FUserManager_00601_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd56cb0a089b14dab96ad3ee133819f966d938_003F9c_003F183f8355_003FUserManager_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValueTuple_00602_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fa7_003F76eb4679_003FValueTuple_00602_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
|
||||
</TestAncestor>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f3f8a684_002Dc08e_002D489f_002D949c_002D6c38a1ed63b0/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
|
||||
</TestAncestor>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f3f8a684_002Dc08e_002D489f_002D949c_002D6c38a1ed63b0/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String></wpf:ResourceDictionary>
|
||||
@ -6,4 +6,10 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Identity.Core">
|
||||
<HintPath>..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\9.0.2\Microsoft.Extensions.Identity.Core.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
namespace Flawless.Communication.Request.Auth;
|
||||
|
||||
public record RegisterRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
|
||||
public string Username { get; set; }
|
||||
|
||||
public string Password { get; set; }
|
||||
}
|
||||
3
Flawless.Communication/Request/LocateUserRequest.cs
Normal file
3
Flawless.Communication/Request/LocateUserRequest.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
public record LocateUserRequest(string? UserId, string? Username);
|
||||
@ -1,4 +1,4 @@
|
||||
namespace Flawless.Communication.Request.Auth;
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
public record LoginRequest
|
||||
{
|
||||
10
Flawless.Communication/Request/QueryPagesRequest.cs
Normal file
10
Flawless.Communication/Request/QueryPagesRequest.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
public class QueryPagesRequest<T>
|
||||
{
|
||||
public required int Offset { get; init; }
|
||||
|
||||
public required int Length { get; init; }
|
||||
|
||||
public T? Parameter { get; init; }
|
||||
}
|
||||
10
Flawless.Communication/Request/RegisterRequest.cs
Normal file
10
Flawless.Communication/Request/RegisterRequest.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
public record RegisterRequest
|
||||
{
|
||||
public required string Email { get; set; }
|
||||
|
||||
public required string Username { get; set; }
|
||||
|
||||
public required string Password { get; set; }
|
||||
}
|
||||
10
Flawless.Communication/Request/ResetPasswordRequest.cs
Normal file
10
Flawless.Communication/Request/ResetPasswordRequest.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
public class ResetPasswordRequest
|
||||
{
|
||||
public string? Identity { get; set; }
|
||||
|
||||
public string? OldPassword { get; set; }
|
||||
|
||||
public required string NewPassword { get; set; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
public record UserContactModifyResponse
|
||||
{
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Phone { get; set; }
|
||||
}
|
||||
14
Flawless.Communication/Request/UserInfoModifyResponse.cs
Normal file
14
Flawless.Communication/Request/UserInfoModifyResponse.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Flawless.Communication.Shared;
|
||||
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
public record UserInfoModifyResponse
|
||||
{
|
||||
public string? NickName { get; set; }
|
||||
|
||||
public UserSex? Gender { get; set; }
|
||||
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public bool? PublicEmail { get; set; }
|
||||
}
|
||||
@ -1,8 +1,3 @@
|
||||
namespace Flawless.Communication.Response;
|
||||
|
||||
public record FailedResponse(object message, string? failure = "Unknown")
|
||||
{
|
||||
public string? Failure { get; set; } = failure;
|
||||
|
||||
public object? Message { get; set; } = message;
|
||||
}
|
||||
public record FailedResponse(object Message, string? Failure = "Default");
|
||||
25
Flawless.Communication/Response/PagedResponse.cs
Normal file
25
Flawless.Communication/Response/PagedResponse.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace Flawless.Communication.Response;
|
||||
|
||||
public record PagedResponse<T>
|
||||
{
|
||||
public required int Offset { get; init; }
|
||||
|
||||
public required int Length { get; init; }
|
||||
|
||||
public uint? Total { get; init; }
|
||||
|
||||
public IEnumerable<T>? Data { get; init; }
|
||||
}
|
||||
|
||||
public record PagedResponse<TData, TMetadata>
|
||||
{
|
||||
public required int Offset { get; init; }
|
||||
|
||||
public required int Length { get; init; }
|
||||
|
||||
public int? Total { get; init; }
|
||||
|
||||
public IEnumerable<TData>? Data { get; init; }
|
||||
|
||||
public TMetadata? Metadata { get; init; }
|
||||
}
|
||||
@ -1,8 +1,14 @@
|
||||
namespace Flawless.Communication.Response;
|
||||
|
||||
public record UnintendedExceptionResponse(Exception e)
|
||||
public record UnintendedExceptionResponse
|
||||
{
|
||||
public string ExceptionType { get; } = e.GetType().Name;
|
||||
public string ExceptionType { get; }
|
||||
|
||||
public string ExceptionMessage { get; }
|
||||
|
||||
public string ExceptionMessage { get; } = e.Message;
|
||||
public UnintendedExceptionResponse(Exception e)
|
||||
{
|
||||
ExceptionType = e.GetType().FullName;
|
||||
ExceptionMessage = e.Message;
|
||||
}
|
||||
}
|
||||
24
Flawless.Communication/Response/UserInfoResponse.cs
Normal file
24
Flawless.Communication/Response/UserInfoResponse.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Flawless.Communication.Shared;
|
||||
|
||||
namespace Flawless.Communication.Response;
|
||||
|
||||
public record UserInfoResponse
|
||||
{
|
||||
public bool Authorized { get; set; }
|
||||
|
||||
public string? Username { get; set; }
|
||||
|
||||
public string? NickName { get; set; }
|
||||
|
||||
public UserSex? Gender { get; set; }
|
||||
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Phone { get; set; }
|
||||
|
||||
public bool? PublicEmail { get; set; }
|
||||
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Flawless.Communication.Response;
|
||||
|
||||
public class UserLoginHistoryResponse
|
||||
{
|
||||
}
|
||||
9
Flawless.Communication/Shared/UserSex.cs
Normal file
9
Flawless.Communication/Shared/UserSex.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Flawless.Communication.Shared;
|
||||
|
||||
public enum UserSex : byte
|
||||
{
|
||||
Unset = 0,
|
||||
Male = 1,
|
||||
Female = 2,
|
||||
WalmartPlasticBag = 3
|
||||
}
|
||||
67
Flawless.Server/Controllers/AdminUserController.cs
Normal file
67
Flawless.Server/Controllers/AdminUserController.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using Flawless.Communication.Request;
|
||||
using Flawless.Communication.Response;
|
||||
using Flawless.Server.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Flawless.Server.Controllers;
|
||||
|
||||
[ApiController, Authorize, Route("api/admin")]
|
||||
public class AdminUserController(
|
||||
UserManager<AppUser> userManager) : ControllerBase
|
||||
{
|
||||
[HttpPost("user/delete")]
|
||||
public async Task<IActionResult> DeleteUserAsync(LocateUserRequest r)
|
||||
{
|
||||
if (r.UserId == null) return BadRequest(new FailedResponse("User id is not set!"));
|
||||
var user = await userManager.FindByIdAsync(r.UserId);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
var result = await userManager.DeleteAsync(user);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("user/enable")]
|
||||
public async Task<IActionResult> EnableUserAsync(LocateUserRequest r)
|
||||
{
|
||||
if (r.UserId == null) return BadRequest(new FailedResponse("User id is not set!"));
|
||||
var user = await userManager.FindByIdAsync(r.UserId);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
var result = await userManager.SetLockoutEnabledAsync(user, false);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("user/disable")]
|
||||
public async Task<IActionResult> DisableUserAsync(LocateUserRequest r)
|
||||
{
|
||||
if (r.UserId == null) return BadRequest(new FailedResponse("User id is not set!"));
|
||||
var user = await userManager.FindByIdAsync(r.UserId);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
var result = await userManager.SetLockoutEnabledAsync(user, true);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("user/reset_password")]
|
||||
public async Task<IActionResult> ResetPasswordAsync(ResetPasswordRequest r)
|
||||
{
|
||||
if (r.Identity == null) return BadRequest(new FailedResponse("Identity (User Id) is not set!"));
|
||||
var user = await userManager.FindByIdAsync(r.Identity);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("Identity (User Id) does not exist!"));
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var result = await userManager.ResetPasswordAsync(user, resetToken, r.NewPassword);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
using System.Security.Claims;
|
||||
using Flawless.Communication.Request.Auth;
|
||||
using Flawless.Communication.Request;
|
||||
using Flawless.Communication.Response;
|
||||
using Flawless.Communication.Shared;
|
||||
using Flawless.Server.Models;
|
||||
using Flawless.Server.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -22,7 +23,7 @@ public class AuthenticationController(
|
||||
{
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> PublicRegisterAsync(RegisterRequest request)
|
||||
public async Task<ActionResult> PublicRegisterAsync(RegisterRequest request)
|
||||
{
|
||||
if (!config.GetValue("User:PublicRegister", true))
|
||||
return BadRequest(new FailedResponse("Not opened for public register."));
|
||||
@ -49,7 +50,7 @@ public class AuthenticationController(
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> LoginAsync(LoginRequest r)
|
||||
public async Task<ActionResult> LoginAsync(LoginRequest r)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(r.Username);
|
||||
if (user == null) return BadRequest(new FailedResponse("Invalid username or password."));
|
||||
@ -83,12 +84,12 @@ public class AuthenticationController(
|
||||
}
|
||||
|
||||
[HttpPost("refresh")]
|
||||
public async Task<IActionResult> RefreshAsync(TokenInfo r)
|
||||
public async Task<ActionResult<TokenInfo>> RefreshAsync(TokenInfo r)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var set = dbContext.RefreshTokens;
|
||||
var principal = tokenService.GetPrincipalFromExpiredToken(r.Token);
|
||||
var user = await signInManager.ValidateSecurityStampAsync(principal);
|
||||
var user = await userManager.GetUserAsync(principal);
|
||||
if (user == null) return BadRequest(new FailedResponse("Token is ban. Please login again."));
|
||||
|
||||
try
|
||||
@ -124,12 +125,41 @@ public class AuthenticationController(
|
||||
|
||||
}
|
||||
|
||||
[HttpPost("logout_all")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> LogoutAllAsync()
|
||||
{
|
||||
var u = (await userManager.GetUserAsync(User))!;
|
||||
u.RenewSecurityStamp();
|
||||
|
||||
// Do not let tokens can be refresh
|
||||
await dbContext.RefreshTokens.Where(k => k.UserId == u.Id).ExecuteDeleteAsync();
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("renew_password")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> RenewPasswordAsync(ResetPasswordRequest r)
|
||||
{
|
||||
var u = (await userManager.GetUserAsync(User))!;
|
||||
if (string.IsNullOrEmpty(r.OldPassword)) return Unauthorized(new FailedResponse("Old password is empty."));
|
||||
|
||||
var result = await userManager.ChangePasswordAsync(u, r.OldPassword, r.NewPassword);
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
|
||||
u.RenewSecurityStamp();
|
||||
await dbContext.SaveChangesAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private async ValueTask<IList<Claim>> GetClaimsAsync(AppUser user, string refreshToken)
|
||||
{
|
||||
var c = await userManager.GetClaimsAsync(user);
|
||||
c.Add(new (FlawlessClaimsType.SecurityStamp, user.SecurityStamp ?? string.Empty));
|
||||
c.Add(new (ClaimTypes.NameIdentifier, user.Id.ToString()));
|
||||
c.Add(new (ClaimTypes.Name, user.UserName!));
|
||||
c.Add(new (FlawlessClaimsType.RefreshToken, refreshToken));
|
||||
|
||||
return c;
|
||||
|
||||
@ -1,9 +1,152 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Flawless.Communication.Request;
|
||||
using Flawless.Communication.Response;
|
||||
using Flawless.Communication.Shared;
|
||||
using Flawless.Server.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Flawless.Server.Controllers;
|
||||
|
||||
[ApiController, Route("api/user")]
|
||||
public class UserController : ControllerBase
|
||||
[ApiController, Authorize, Route("api/user")]
|
||||
public class UserController(
|
||||
UserManager<AppUser> userManager
|
||||
) : ControllerBase
|
||||
{
|
||||
|
||||
[HttpPost("update/info")]
|
||||
public async Task<IActionResult> UpdateUserInfoAsync(UserInfoModifyResponse r)
|
||||
{
|
||||
bool update = false;
|
||||
bool renew = false;
|
||||
|
||||
// Modify content
|
||||
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
|
||||
if (r.NickName != null)
|
||||
{
|
||||
update = true;
|
||||
u.NickName = r.NickName;
|
||||
}
|
||||
|
||||
if (r.Bio != null)
|
||||
{
|
||||
update = true;
|
||||
u.Bio = r.Bio;
|
||||
}
|
||||
|
||||
if (r.Gender != null)
|
||||
{
|
||||
update = true;
|
||||
u.Gender = r.Gender ?? UserSex.Unset;
|
||||
}
|
||||
|
||||
if (r.PublicEmail != null)
|
||||
{
|
||||
update = true;
|
||||
u.PublicEmail = r.PublicEmail ?? false;
|
||||
}
|
||||
|
||||
|
||||
if (renew) u.RenewSecurityStamp();
|
||||
if (update || renew) await userManager.UpdateAsync(u);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("update/email")]
|
||||
public async Task<IActionResult> UpdateEmailAsync(UserContactModifyResponse r)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(r.Email))
|
||||
return BadRequest(new FailedResponse("No valid email address provided!"));
|
||||
|
||||
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
var result = await userManager.SetEmailAsync(u, r.Email);
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("update/phone")]
|
||||
public async Task<IActionResult> UpdatePhoneAsync(UserContactModifyResponse r)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(r.Phone))
|
||||
return BadRequest(new FailedResponse("No valid phone number provided!"));
|
||||
|
||||
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
var result = await userManager.SetPhoneNumberAsync(u, r.Phone);
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("get/info")]
|
||||
public async Task<ActionResult<UserInfoResponse>> GetUserInfoAsync(LocateUserRequest r)
|
||||
{
|
||||
var self = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
|
||||
if (r.UserId != null)
|
||||
{
|
||||
var u = await userManager.FindByIdAsync(r.UserId);
|
||||
if (u == null) return BadRequest(new FailedResponse("User is not existed!"));
|
||||
|
||||
return Ok(GetUserInfoInternal(u, self));
|
||||
}
|
||||
|
||||
if (r.Username != null)
|
||||
{
|
||||
var u = await userManager.FindByNameAsync(r.Username);
|
||||
if (u == null) return BadRequest(new FailedResponse("User is not existed!"));
|
||||
|
||||
return Ok(GetUserInfoInternal(u, self));
|
||||
}
|
||||
|
||||
// Return self as default
|
||||
return Ok(GetUserInfoInternal(self, self));
|
||||
}
|
||||
|
||||
[HttpGet("query/info")]
|
||||
public async Task<ActionResult<PagedResponse<UserInfoResponse>>> GetUserInfoAsync(QueryPagesRequest<LocateUserRequest> r)
|
||||
{
|
||||
var queryNamePrefix = r.Parameter?.Username ?? String.Empty;
|
||||
var queryId = r.Parameter == null ? Guid.Empty : Guid.Parse(r.Parameter.UserId!);
|
||||
var payload = await userManager.Users
|
||||
.Where(u => u.UserName!.StartsWith(queryNamePrefix) || u.Id == queryId)
|
||||
.Skip(r.Offset)
|
||||
.Take(r.Length)
|
||||
.Select(u => GetUserInfoInternal(u, null))
|
||||
.ToArrayAsync();
|
||||
|
||||
// Return self as default
|
||||
return Ok(new PagedResponse<UserInfoResponse>
|
||||
{
|
||||
Offset = r.Offset,
|
||||
Length = r.Length,
|
||||
Data = payload
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("delete")]
|
||||
public async Task<IActionResult> DeleteUserAsync()
|
||||
{
|
||||
var self = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
await userManager.DeleteAsync(self);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
private UserInfoResponse GetUserInfoInternal(AppUser queryUser, AppUser? currentUser)
|
||||
{
|
||||
var authorized = queryUser.Id == currentUser?.Id;
|
||||
return new UserInfoResponse
|
||||
{
|
||||
Username = queryUser.UserName,
|
||||
CreatedAt = queryUser.CreatedOn,
|
||||
Bio = queryUser.Bio,
|
||||
Gender = queryUser.Gender,
|
||||
NickName = queryUser.NickName,
|
||||
Email = queryUser.PublicEmail || authorized ? queryUser.Email : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -37,8 +37,4 @@
|
||||
<Compile Remove="Migrations\20250322194407_InitialCreate.Designer.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,11 +1,25 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Flawless.Communication.Shared;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Flawless.Server.Models;
|
||||
|
||||
|
||||
public class AppUser : IdentityUser<Guid>
|
||||
{
|
||||
public DateTime CreatedOn { get; set; }
|
||||
|
||||
public UserSex Gender { get; set; }
|
||||
|
||||
[MaxLength(50)]
|
||||
public string? NickName { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public bool PublicEmail { get; set; }
|
||||
|
||||
|
||||
public void RenewSecurityStamp()
|
||||
{
|
||||
this.SecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flawless.Communication.Response;
|
||||
using Flawless.Server.Middlewares;
|
||||
using Flawless.Server.Models;
|
||||
using Flawless.Server.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
@ -32,7 +34,11 @@ public static class Program
|
||||
{
|
||||
// Api related
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(opt =>
|
||||
{
|
||||
opt.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
});
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(opt =>
|
||||
{
|
||||
@ -62,7 +68,6 @@ public static class Program
|
||||
opt.SignIn.RequireConfirmedEmail = false;
|
||||
opt.SignIn.RequireConfirmedPhoneNumber = false;
|
||||
opt.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier;
|
||||
opt.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name;
|
||||
opt.ClaimsIdentity.SecurityStampClaimType = FlawlessClaimsType.SecurityStamp;
|
||||
})
|
||||
.AddSignInManager()
|
||||
@ -75,7 +80,6 @@ public static class Program
|
||||
var secretKey = config["Jwt:SecretKey"] ?? throw new ApplicationException("No secret key found.");
|
||||
var issuer = config["Jwt:Issuer"] ?? throw new ApplicationException("No issuer found.");
|
||||
|
||||
|
||||
// Authentication related.
|
||||
builder.Services.AddSingleton<TokenGenerationService>();
|
||||
builder.Services.AddAuthentication(opt =>
|
||||
@ -96,45 +100,12 @@ public static class Program
|
||||
RoleClaimType = ClaimTypes.Role,
|
||||
NameClaimType = ClaimTypes.Name,
|
||||
};
|
||||
|
||||
|
||||
opt.Events = new JwtBearerEvents {
|
||||
OnAuthenticationFailed = context =>
|
||||
{
|
||||
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) {
|
||||
context.Response.Headers.Append("Token-Expired", "true");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnTokenValidated = async context =>
|
||||
{
|
||||
if (context.Principal != null)
|
||||
{
|
||||
var p = context.Principal!;
|
||||
|
||||
var id = p.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (id == null) throw new SecurityTokenExpiredException("User is not defined in the token!");
|
||||
|
||||
var stamp = p.FindFirst(FlawlessClaimsType.SecurityStamp)?.Value;
|
||||
if (stamp == null) throw new SecurityTokenExpiredException("No valid SecurityStamp found.");
|
||||
|
||||
var db = context.HttpContext.RequestServices.GetRequiredService<UserManager<AppUser>>();
|
||||
|
||||
// Start validate user is existed and not expired.
|
||||
var u = await db.FindByIdAsync(id!);
|
||||
if (u == null) throw new SecurityTokenExpiredException("User is not existed in db.");
|
||||
if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched.");
|
||||
|
||||
context.HttpContext.User = p;
|
||||
context.Success();
|
||||
}
|
||||
}
|
||||
OnAuthenticationFailed = OnAuthenticationFailedAsync,
|
||||
OnTokenValidated = OnTokenValidatedAsync
|
||||
};
|
||||
});
|
||||
// builder.Services.AddAuthorization(opt =>
|
||||
// {
|
||||
// opt.DefaultPolicy =
|
||||
// })
|
||||
}
|
||||
|
||||
private static void SetupWebApplication(WebApplication app)
|
||||
@ -170,4 +141,45 @@ public static class Program
|
||||
app.MapGet("/", () => "<p>Please use client app to open this server.</p>");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task OnTokenValidatedAsync(TokenValidatedContext context)
|
||||
{
|
||||
if (context.Principal != null)
|
||||
{
|
||||
var p = context.Principal!;
|
||||
var auth = context.HttpContext.GetEndpoint()?.Metadata.GetOrderedMetadata<AuthorizeAttribute>();
|
||||
|
||||
if (auth?.Any() ?? false)
|
||||
{
|
||||
var id = p.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (id == null) throw new SecurityTokenExpiredException("User is not defined in the token!");
|
||||
|
||||
var stamp = p.FindFirst(FlawlessClaimsType.SecurityStamp)?.Value;
|
||||
if (stamp == null) throw new SecurityTokenExpiredException("No valid SecurityStamp found.");
|
||||
|
||||
// Validate user status
|
||||
var db = context.HttpContext.RequestServices.GetRequiredService<UserManager<AppUser>>();
|
||||
|
||||
var u = await db.FindByIdAsync(id!);
|
||||
if (u == null) throw new SecurityTokenExpiredException("User is not existed.");
|
||||
|
||||
if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched.");
|
||||
if (u.LockoutEnabled) throw new SecurityTokenExpiredException("User has been locked.");
|
||||
}
|
||||
|
||||
// Extract user info into HttpContext
|
||||
context.HttpContext.User = p;
|
||||
context.Success();
|
||||
}
|
||||
}
|
||||
|
||||
private static Task OnAuthenticationFailedAsync(AuthenticationFailedContext context)
|
||||
{
|
||||
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
|
||||
{
|
||||
context.Response.Headers.Append("Token-Expired", "true");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@ -9,4 +9,5 @@ public class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
: IdentityDbContext<AppUser, IdentityRole<Guid>, Guid>(options)
|
||||
{
|
||||
public DbSet<AppUserRefreshKey> RefreshTokens { get; set; }
|
||||
|
||||
}
|
||||
@ -11,7 +11,7 @@
|
||||
},
|
||||
"LocalStoragePath": "./data/development",
|
||||
"User": {
|
||||
"PublicRegister": false
|
||||
"PublicRegister": true
|
||||
},
|
||||
"Jwt": {
|
||||
"SecretKey": "your_256bit_security_key_at_here_otherwise_not_bootable",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user