diff --git a/Flawless-Version-Control.sln.DotSettings.user b/Flawless-Version-Control.sln.DotSettings.user index d55441a..7c73cf0 100644 --- a/Flawless-Version-Control.sln.DotSettings.user +++ b/Flawless-Version-Control.sln.DotSettings.user @@ -6,6 +6,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -18,15 +19,16 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded - <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 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> - <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 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> \ No newline at end of file diff --git a/Flawless.Communication/Flawless.Communication.csproj b/Flawless.Communication/Flawless.Communication.csproj index 17b910f..4247cd9 100644 --- a/Flawless.Communication/Flawless.Communication.csproj +++ b/Flawless.Communication/Flawless.Communication.csproj @@ -6,4 +6,10 @@ enable + + + ..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\9.0.2\Microsoft.Extensions.Identity.Core.dll + + + diff --git a/Flawless.Communication/Request/Auth/RegisterRequest.cs b/Flawless.Communication/Request/Auth/RegisterRequest.cs deleted file mode 100644 index aab3e4b..0000000 --- a/Flawless.Communication/Request/Auth/RegisterRequest.cs +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/Flawless.Communication/Request/LocateUserRequest.cs b/Flawless.Communication/Request/LocateUserRequest.cs new file mode 100644 index 0000000..428544e --- /dev/null +++ b/Flawless.Communication/Request/LocateUserRequest.cs @@ -0,0 +1,3 @@ +namespace Flawless.Communication.Request; + +public record LocateUserRequest(string? UserId, string? Username); \ No newline at end of file diff --git a/Flawless.Communication/Request/Auth/LoginRequest.cs b/Flawless.Communication/Request/LoginRequest.cs similarity index 73% rename from Flawless.Communication/Request/Auth/LoginRequest.cs rename to Flawless.Communication/Request/LoginRequest.cs index 149f062..9d46958 100644 --- a/Flawless.Communication/Request/Auth/LoginRequest.cs +++ b/Flawless.Communication/Request/LoginRequest.cs @@ -1,4 +1,4 @@ -namespace Flawless.Communication.Request.Auth; +namespace Flawless.Communication.Request; public record LoginRequest { diff --git a/Flawless.Communication/Request/QueryPagesRequest.cs b/Flawless.Communication/Request/QueryPagesRequest.cs new file mode 100644 index 0000000..cdb29e0 --- /dev/null +++ b/Flawless.Communication/Request/QueryPagesRequest.cs @@ -0,0 +1,10 @@ +namespace Flawless.Communication.Request; + +public class QueryPagesRequest +{ + public required int Offset { get; init; } + + public required int Length { get; init; } + + public T? Parameter { get; init; } +} \ No newline at end of file diff --git a/Flawless.Communication/Request/RegisterRequest.cs b/Flawless.Communication/Request/RegisterRequest.cs new file mode 100644 index 0000000..3a6a4eb --- /dev/null +++ b/Flawless.Communication/Request/RegisterRequest.cs @@ -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; } +} \ No newline at end of file diff --git a/Flawless.Communication/Request/ResetPasswordRequest.cs b/Flawless.Communication/Request/ResetPasswordRequest.cs new file mode 100644 index 0000000..d3662e5 --- /dev/null +++ b/Flawless.Communication/Request/ResetPasswordRequest.cs @@ -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; } +} \ No newline at end of file diff --git a/Flawless.Communication/Request/UserContactModifyResponse.cs b/Flawless.Communication/Request/UserContactModifyResponse.cs new file mode 100644 index 0000000..7561004 --- /dev/null +++ b/Flawless.Communication/Request/UserContactModifyResponse.cs @@ -0,0 +1,8 @@ +namespace Flawless.Communication.Request; + +public record UserContactModifyResponse +{ + public string? Email { get; set; } + + public string? Phone { get; set; } +} \ No newline at end of file diff --git a/Flawless.Communication/Request/UserInfoModifyResponse.cs b/Flawless.Communication/Request/UserInfoModifyResponse.cs new file mode 100644 index 0000000..37ca4e2 --- /dev/null +++ b/Flawless.Communication/Request/UserInfoModifyResponse.cs @@ -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; } +} \ No newline at end of file diff --git a/Flawless.Communication/Response/FailedResponse.cs b/Flawless.Communication/Response/FailedResponse.cs index 5594aee..fcd5610 100644 --- a/Flawless.Communication/Response/FailedResponse.cs +++ b/Flawless.Communication/Response/FailedResponse.cs @@ -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; -} \ No newline at end of file +public record FailedResponse(object Message, string? Failure = "Default"); \ No newline at end of file diff --git a/Flawless.Communication/Response/PagedResponse.cs b/Flawless.Communication/Response/PagedResponse.cs new file mode 100644 index 0000000..bfa9d01 --- /dev/null +++ b/Flawless.Communication/Response/PagedResponse.cs @@ -0,0 +1,25 @@ +namespace Flawless.Communication.Response; + +public record PagedResponse +{ + public required int Offset { get; init; } + + public required int Length { get; init; } + + public uint? Total { get; init; } + + public IEnumerable? Data { get; init; } +} + +public record PagedResponse +{ + public required int Offset { get; init; } + + public required int Length { get; init; } + + public int? Total { get; init; } + + public IEnumerable? Data { get; init; } + + public TMetadata? Metadata { get; init; } +} \ No newline at end of file diff --git a/Flawless.Communication/Response/UnintendedExceptionResponse.cs b/Flawless.Communication/Response/UnintendedExceptionResponse.cs index 3b03e69..01ddb6b 100644 --- a/Flawless.Communication/Response/UnintendedExceptionResponse.cs +++ b/Flawless.Communication/Response/UnintendedExceptionResponse.cs @@ -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; + } } \ No newline at end of file diff --git a/Flawless.Communication/Response/UserInfoResponse.cs b/Flawless.Communication/Response/UserInfoResponse.cs new file mode 100644 index 0000000..fc72929 --- /dev/null +++ b/Flawless.Communication/Response/UserInfoResponse.cs @@ -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; } +} \ No newline at end of file diff --git a/Flawless.Communication/Response/UserLoginHistoryResponse.cs b/Flawless.Communication/Response/UserLoginHistoryResponse.cs new file mode 100644 index 0000000..c7b3de8 --- /dev/null +++ b/Flawless.Communication/Response/UserLoginHistoryResponse.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.Identity; + +namespace Flawless.Communication.Response; + +public class UserLoginHistoryResponse +{ +} \ No newline at end of file diff --git a/Flawless.Communication/Shared/UserSex.cs b/Flawless.Communication/Shared/UserSex.cs new file mode 100644 index 0000000..8a5f047 --- /dev/null +++ b/Flawless.Communication/Shared/UserSex.cs @@ -0,0 +1,9 @@ +namespace Flawless.Communication.Shared; + +public enum UserSex : byte +{ + Unset = 0, + Male = 1, + Female = 2, + WalmartPlasticBag = 3 +} \ No newline at end of file diff --git a/Flawless.Server/Controllers/AdminUserController.cs b/Flawless.Server/Controllers/AdminUserController.cs new file mode 100644 index 0000000..2c71f89 --- /dev/null +++ b/Flawless.Server/Controllers/AdminUserController.cs @@ -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 userManager) : ControllerBase +{ + [HttpPost("user/delete")] + public async Task 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 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 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 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(); + } + +} \ No newline at end of file diff --git a/Flawless.Server/Controllers/AuthenticationController.cs b/Flawless.Server/Controllers/AuthenticationController.cs index 244ff82..cd2c93c 100644 --- a/Flawless.Server/Controllers/AuthenticationController.cs +++ b/Flawless.Server/Controllers/AuthenticationController.cs @@ -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 PublicRegisterAsync(RegisterRequest request) + public async Task 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 LoginAsync(LoginRequest r) + public async Task 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 RefreshAsync(TokenInfo r) + public async Task> 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 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 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> 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; diff --git a/Flawless.Server/Controllers/UserController.cs b/Flawless.Server/Controllers/UserController.cs index 0343c5c..3bb03a2 100644 --- a/Flawless.Server/Controllers/UserController.cs +++ b/Flawless.Server/Controllers/UserController.cs @@ -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 userManager + ) : ControllerBase { + [HttpPost("update/info")] + public async Task 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 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 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> 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>> GetUserInfoAsync(QueryPagesRequest 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 + { + Offset = r.Offset, + Length = r.Length, + Data = payload + }); + } + + [HttpGet("delete")] + public async Task 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, + }; + } } \ No newline at end of file diff --git a/Flawless.Server/Flawless.Server.csproj b/Flawless.Server/Flawless.Server.csproj index e787c38..e5322e7 100644 --- a/Flawless.Server/Flawless.Server.csproj +++ b/Flawless.Server/Flawless.Server.csproj @@ -37,8 +37,4 @@ - - - - diff --git a/Flawless.Server/Models/AppUser.cs b/Flawless.Server/Models/AppUser.cs index ee948cb..5aee95d 100644 --- a/Flawless.Server/Models/AppUser.cs +++ b/Flawless.Server/Models/AppUser.cs @@ -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 { 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(); diff --git a/Flawless.Server/Program.cs b/Flawless.Server/Program.cs index 2ad63c1..bb062aa 100644 --- a/Flawless.Server/Program.cs +++ b/Flawless.Server/Program.cs @@ -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(); 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>(); - - // 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("/", () => "

Please use client app to open this server.

"); } } + + private static async Task OnTokenValidatedAsync(TokenValidatedContext context) + { + if (context.Principal != null) + { + var p = context.Principal!; + var auth = context.HttpContext.GetEndpoint()?.Metadata.GetOrderedMetadata(); + + 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>(); + + 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; + } } \ No newline at end of file diff --git a/Flawless.Server/Services/AppDbContext.cs b/Flawless.Server/Services/AppDbContext.cs index 1fe39a5..eae992e 100644 --- a/Flawless.Server/Services/AppDbContext.cs +++ b/Flawless.Server/Services/AppDbContext.cs @@ -9,4 +9,5 @@ public class AppDbContext(DbContextOptions options) : IdentityDbContext, Guid>(options) { public DbSet RefreshTokens { get; set; } + } \ No newline at end of file diff --git a/Flawless.Server/appsettings.Development.json b/Flawless.Server/appsettings.Development.json index d4ee3dc..5b1be17 100644 --- a/Flawless.Server/appsettings.Development.json +++ b/Flawless.Server/appsettings.Development.json @@ -11,7 +11,7 @@ }, "LocalStoragePath": "./data/development", "User": { - "PublicRegister": false + "PublicRegister": true }, "Jwt": { "SecretKey": "your_256bit_security_key_at_here_otherwise_not_bootable",