From bc87a406ebb8aa9d595526e4fc3b726f7ef5ad13 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 12 Nov 2020 16:42:46 +0800 Subject: refactor(database): Add user permission table. --- BackEnd/Timeline/Entities/UserEntity.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'BackEnd/Timeline/Entities/UserEntity.cs') diff --git a/BackEnd/Timeline/Entities/UserEntity.cs b/BackEnd/Timeline/Entities/UserEntity.cs index 0cfaa335..83fe9aea 100644 --- a/BackEnd/Timeline/Entities/UserEntity.cs +++ b/BackEnd/Timeline/Entities/UserEntity.cs @@ -11,7 +11,6 @@ namespace Timeline.Entities public const string User = "user"; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is an entity class.")] [Table("users")] public class UserEntity { @@ -47,10 +46,14 @@ namespace Timeline.Entities public UserAvatarEntity? Avatar { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only + public List Permissions { get; set; } = default!; + public List Timelines { get; set; } = default!; public List TimelinePosts { get; set; } = default!; public List TimelinesJoined { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only } } -- cgit v1.2.3 From e4c4a284571d51dcda373a0a1c047e634b17882d Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 12 Nov 2020 21:38:43 +0800 Subject: ... --- .../IntegratedTests/AuthorizationTest.cs | 52 ----- BackEnd/Timeline/Auth/Attribute.cs | 21 -- BackEnd/Timeline/Auth/MyAuthenticationHandler.cs | 9 +- .../Timeline/Auth/PermissionAuthorizeAttribute.cs | 30 +++ BackEnd/Timeline/Auth/PermissionPolicyProvider.cs | 35 ++++ BackEnd/Timeline/Auth/PrincipalExtensions.cs | 10 +- .../Controllers/Testing/TestingAuthController.cs | 32 --- BackEnd/Timeline/Entities/UserEntity.cs | 6 - BackEnd/Timeline/Models/Http/UserInfo.cs | 21 +- BackEnd/Timeline/Models/User.cs | 28 +-- BackEnd/Timeline/Services/UserRoleConvert.cs | 43 ---- BackEnd/Timeline/Services/UserService.cs | 230 ++++++--------------- BackEnd/Timeline/Services/UserTokenManager.cs | 6 +- BackEnd/Timeline/Startup.cs | 4 + 14 files changed, 182 insertions(+), 345 deletions(-) delete mode 100644 BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs delete mode 100644 BackEnd/Timeline/Auth/Attribute.cs create mode 100644 BackEnd/Timeline/Auth/PermissionAuthorizeAttribute.cs create mode 100644 BackEnd/Timeline/Auth/PermissionPolicyProvider.cs delete mode 100644 BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs delete mode 100644 BackEnd/Timeline/Services/UserRoleConvert.cs (limited to 'BackEnd/Timeline/Entities/UserEntity.cs') diff --git a/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs deleted file mode 100644 index 38071394..00000000 --- a/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs +++ /dev/null @@ -1,52 +0,0 @@ -using FluentAssertions; -using System.Net; -using System.Threading.Tasks; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class AuthorizationTest : IntegratedTestBase - { - private const string BaseUrl = "testing/auth/"; - private const string AuthorizeUrl = BaseUrl + "Authorize"; - private const string UserUrl = BaseUrl + "User"; - private const string AdminUrl = BaseUrl + "Admin"; - - [Fact] - public async Task UnauthenticationTest() - { - using var client = await CreateDefaultClient(); - var response = await client.GetAsync(AuthorizeUrl); - response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - - [Fact] - public async Task AuthenticationTest() - { - using var client = await CreateClientAsUser(); - var response = await client.GetAsync(AuthorizeUrl); - response.Should().HaveStatusCode(HttpStatusCode.OK); - } - - [Fact] - public async Task UserAuthorizationTest() - { - using var client = await CreateClientAsUser(); - var response1 = await client.GetAsync(UserUrl); - response1.Should().HaveStatusCode(HttpStatusCode.OK); - var response2 = await client.GetAsync(AdminUrl); - response2.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - - [Fact] - public async Task AdminAuthorizationTest() - { - using var client = await CreateClientAsAdministrator(); - var response1 = await client.GetAsync(UserUrl); - response1.Should().HaveStatusCode(HttpStatusCode.OK); - var response2 = await client.GetAsync(AdminUrl); - response2.Should().HaveStatusCode(HttpStatusCode.OK); - } - } -} diff --git a/BackEnd/Timeline/Auth/Attribute.cs b/BackEnd/Timeline/Auth/Attribute.cs deleted file mode 100644 index 86d0109b..00000000 --- a/BackEnd/Timeline/Auth/Attribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Timeline.Entities; - -namespace Timeline.Auth -{ - public class AdminAuthorizeAttribute : AuthorizeAttribute - { - public AdminAuthorizeAttribute() - { - Roles = UserRoles.Admin; - } - } - - public class UserAuthorizeAttribute : AuthorizeAttribute - { - public UserAuthorizeAttribute() - { - Roles = UserRoles.User; - } - } -} diff --git a/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs index 3c97c329..b5e22a14 100644 --- a/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs +++ b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs @@ -17,6 +17,7 @@ namespace Timeline.Auth { public const string Scheme = "Bearer"; public const string DisplayName = "My Jwt Auth Scheme"; + public const string PermissionClaimName = "Permission"; } public class MyAuthenticationOptions : AuthenticationSchemeOptions @@ -78,12 +79,12 @@ namespace Timeline.Auth try { - var userInfo = await _userTokenManager.VerifyToken(token); + var user = await _userTokenManager.VerifyToken(token); var identity = new ClaimsIdentity(AuthenticationConstants.Scheme); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userInfo.Id!.Value.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); - identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); - identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator!.Value).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); + identity.AddClaim(new Claim(identity.NameClaimType, user.Username, ClaimValueTypes.String)); + identity.AddClaims(user.Permissions.Select(permission => new Claim(AuthenticationConstants.PermissionClaimName, permission.ToString(), ClaimValueTypes.String))); var principal = new ClaimsPrincipal(); principal.AddIdentity(identity); diff --git a/BackEnd/Timeline/Auth/PermissionAuthorizeAttribute.cs b/BackEnd/Timeline/Auth/PermissionAuthorizeAttribute.cs new file mode 100644 index 00000000..3df8dee5 --- /dev/null +++ b/BackEnd/Timeline/Auth/PermissionAuthorizeAttribute.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Authorization; +using System; +using System.Linq; +using Timeline.Services; + +namespace Timeline.Auth +{ + public class PermissionAuthorizeAttribute : AuthorizeAttribute + { + public PermissionAuthorizeAttribute() + { + + } + + public PermissionAuthorizeAttribute(params UserPermission[] permissions) + { + Permissions = permissions; + } + + public UserPermission[] Permissions + { + get => Policy == null ? Array.Empty() : Policy[PermissionPolicyProvider.PolicyPrefix.Length..].Split(',') + .Select(s => Enum.Parse(s)).ToArray(); + set + { + Policy = $"{PermissionPolicyProvider.PolicyPrefix}{string.Join(',', value)}"; + } + } + } +} diff --git a/BackEnd/Timeline/Auth/PermissionPolicyProvider.cs b/BackEnd/Timeline/Auth/PermissionPolicyProvider.cs new file mode 100644 index 00000000..12a4fcd5 --- /dev/null +++ b/BackEnd/Timeline/Auth/PermissionPolicyProvider.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; +using System; +using System.Threading.Tasks; + +namespace Timeline.Auth +{ + public class PermissionPolicyProvider : IAuthorizationPolicyProvider + { + public const string PolicyPrefix = "Permission-"; + + public Task GetDefaultPolicyAsync() + { + return Task.FromResult(new AuthorizationPolicyBuilder(AuthenticationConstants.Scheme).RequireAuthenticatedUser().Build()); + } + + public Task GetFallbackPolicyAsync() + { + return Task.FromResult(null); + } + + public Task GetPolicyAsync(string policyName) + { + if (policyName.StartsWith(PolicyPrefix, StringComparison.OrdinalIgnoreCase)) + { + var permissions = policyName[PolicyPrefix.Length..].Split(','); + + var policy = new AuthorizationPolicyBuilder(AuthenticationConstants.Scheme); + policy.AddRequirements(new ClaimsAuthorizationRequirement(AuthenticationConstants.PermissionClaimName, permissions)); + return Task.FromResult(policy.Build()); + } + return Task.FromResult(null); + } + } +} diff --git a/BackEnd/Timeline/Auth/PrincipalExtensions.cs b/BackEnd/Timeline/Auth/PrincipalExtensions.cs index ad7a887f..9f86e8ac 100644 --- a/BackEnd/Timeline/Auth/PrincipalExtensions.cs +++ b/BackEnd/Timeline/Auth/PrincipalExtensions.cs @@ -1,13 +1,15 @@ -using System.Security.Principal; -using Timeline.Entities; +using System; +using System.Security.Claims; +using Timeline.Services; namespace Timeline.Auth { internal static class PrincipalExtensions { - internal static bool IsAdministrator(this IPrincipal principal) + internal static bool HasPermission(this ClaimsPrincipal principal, UserPermission permission) { - return principal.IsInRole(UserRoles.Admin); + return principal.HasClaim( + claim => claim.Type == AuthenticationConstants.PermissionClaimName && string.Equals(claim.Value, permission.ToString(), StringComparison.InvariantCultureIgnoreCase)); } } } diff --git a/BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs b/BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs deleted file mode 100644 index 4d3b3ec7..00000000 --- a/BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Timeline.Auth; - -namespace Timeline.Controllers.Testing -{ - [Route("testing/auth")] - [ApiController] - public class TestingAuthController : Controller - { - [HttpGet("[action]")] - [Authorize] - public ActionResult Authorize() - { - return Ok(); - } - - [HttpGet("[action]")] - [UserAuthorize] - public new ActionResult User() - { - return Ok(); - } - - [HttpGet("[action]")] - [AdminAuthorize] - public ActionResult Admin() - { - return Ok(); - } - } -} diff --git a/BackEnd/Timeline/Entities/UserEntity.cs b/BackEnd/Timeline/Entities/UserEntity.cs index 83fe9aea..83fa8f89 100644 --- a/BackEnd/Timeline/Entities/UserEntity.cs +++ b/BackEnd/Timeline/Entities/UserEntity.cs @@ -5,12 +5,6 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Timeline.Entities { - public static class UserRoles - { - public const string Admin = "admin"; - public const string User = "user"; - } - [Table("users")] public class UserEntity { diff --git a/BackEnd/Timeline/Models/Http/UserInfo.cs b/BackEnd/Timeline/Models/Http/UserInfo.cs index d92a12c4..26b04e90 100644 --- a/BackEnd/Timeline/Models/Http/UserInfo.cs +++ b/BackEnd/Timeline/Models/Http/UserInfo.cs @@ -2,7 +2,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; +using System.Collections.Generic; using Timeline.Controllers; +using Timeline.Services; namespace Timeline.Models.Http { @@ -27,6 +29,12 @@ namespace Timeline.Models.Http /// True if the user is a administrator. /// public bool? Administrator { get; set; } = default!; +#pragma warning disable CA2227 // Collection properties should be read only + /// + /// The permissions of the user. + /// + public List Permissions { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only #pragma warning disable CA1707 // Identifiers should not contain underscores /// /// Related links. @@ -54,6 +62,14 @@ namespace Timeline.Models.Http public string Timeline { get; set; } = default!; } + public class UserPermissionsValueConverter : ITypeConverter> + { + public List Convert(UserPermissions source, List destination, ResolutionContext context) + { + return source.ToStringList(); + } + } + public class UserInfoLinksValueResolver : IValueResolver { private readonly IActionContextAccessor _actionContextAccessor; @@ -84,7 +100,10 @@ namespace Timeline.Models.Http { public UserInfoAutoMapperProfile() { - CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); + CreateMap>() + .ConvertUsing(); + CreateMap() + .ForMember(u => u._links, opt => opt.MapFrom()); } } } diff --git a/BackEnd/Timeline/Models/User.cs b/BackEnd/Timeline/Models/User.cs index f08a62db..1e90cd1d 100644 --- a/BackEnd/Timeline/Models/User.cs +++ b/BackEnd/Timeline/Models/User.cs @@ -1,21 +1,23 @@ using System; +using Timeline.Services; namespace Timeline.Models { - public class User + public record User { - public string? UniqueId { get; set; } - public string? Username { get; set; } - public string? Nickname { get; set; } - public bool? Administrator { get; set; } + public long Id { get; set; } + public string UniqueId { get; set; } = default!; - #region secret - public long? Id { get; set; } - public string? Password { get; set; } - public long? Version { get; set; } - public DateTime? UsernameChangeTime { get; set; } - public DateTime? CreateTime { get; set; } - public DateTime? LastModified { get; set; } - #endregion secret + public string Username { get; set; } = default!; + public string Nickname { get; set; } = default!; + + [Obsolete("Use permissions instead.")] + public bool Administrator { get; set; } + public UserPermissions Permissions { get; set; } = default!; + + public DateTime UsernameChangeTime { get; set; } + public DateTime CreateTime { get; set; } + public DateTime LastModified { get; set; } + public long Version { get; set; } } } diff --git a/BackEnd/Timeline/Services/UserRoleConvert.cs b/BackEnd/Timeline/Services/UserRoleConvert.cs deleted file mode 100644 index f27ee1bb..00000000 --- a/BackEnd/Timeline/Services/UserRoleConvert.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Timeline.Entities; - -namespace Timeline.Services -{ - public static class UserRoleConvert - { - public const string UserRole = UserRoles.User; - public const string AdminRole = UserRoles.Admin; - - public static string[] ToArray(bool administrator) - { - return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; - } - - public static string[] ToArray(string s) - { - return s.Split(',').ToArray(); - } - - public static bool ToBool(IReadOnlyCollection roles) - { - return roles.Contains(AdminRole); - } - - public static string ToString(IReadOnlyCollection roles) - { - return string.Join(',', roles); - } - - public static string ToString(bool administrator) - { - return administrator ? UserRole + "," + AdminRole : UserRole; - } - - public static bool ToBool(string s) - { - return s.Contains("admin", StringComparison.InvariantCulture); - } - } -} diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 821bc33d..ed637ba3 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; @@ -13,6 +14,11 @@ using static Timeline.Resources.Services.UserService; namespace Timeline.Services { + /// + /// Null means not change. + /// + public record ModifyUserParams(string? Username, string? Password, string? Nickname); + public interface IUserService { /// @@ -33,17 +39,7 @@ namespace Timeline.Services /// The id of the user. /// The user info. /// Thrown when the user with given id does not exist. - Task GetUserById(long id); - - /// - /// Get the user info of given username. - /// - /// Username of the user. - /// The info of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user with given username does not exist. - Task GetUserByUsername(string username); + Task GetUser(long id); /// /// Get the user id of given username. @@ -59,71 +55,31 @@ namespace Timeline.Services /// List all users. /// /// The user info of users. - Task GetUsers(); + Task> GetUsers(); /// /// Create a user with given info. /// - /// The info of new user. + /// The username of new user. + /// The password of new user. /// The the new user. - /// Thrown when is null. - /// Thrown when some fields in is bad. + /// Thrown when or is null. + /// Thrown when or is of bad format. /// Thrown when a user with given username already exists. - /// - /// must not be null and must be a valid username. - /// must not be null or empty. - /// is false by default (null). - /// must be a valid nickname if set. It is empty by default. - /// Other fields are ignored. - /// - Task CreateUser(User info); + Task CreateUser(string username, string password); /// - /// Modify a user's info. + /// Modify a user. /// /// The id of the user. - /// The new info. May be null. + /// The new information. /// The new user info. /// Thrown when some fields in is bad. /// Thrown when user with given id does not exist. /// - /// Only , , and will be used. - /// If null, then not change. - /// Other fields are ignored. /// Version will increase if password is changed. - /// - /// must be a valid username if set. - /// can't be empty if set. - /// must be a valid nickname if set. - /// - /// - /// - Task ModifyUser(long id, User? info); - - /// - /// Modify a user's info. - /// - /// The username of the user. - /// The new info. May be null. - /// The new user info. - /// Thrown when is null. - /// Thrown when is of bad format or some fields in is bad. - /// Thrown when user with given id does not exist. - /// Thrown when user with the newusername already exist. - /// - /// Only , and will be used. - /// If null, then not change. - /// Other fields are ignored. - /// After modified, even if nothing is changed, version will increase. - /// - /// must be a valid username if set. - /// can't be empty if set. - /// must be a valid nickname if set. - /// - /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. /// - /// - Task ModifyUser(string username, User? info); + Task ModifyUser(long id, ModifyUserParams? param); /// /// Try to change a user's password with old password. @@ -146,15 +102,18 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; private readonly IPasswordService _passwordService; + private readonly IUserPermissionService _userPermissionService; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); - public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock) + + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService) { _logger = logger; _clock = clock; _databaseContext = databaseContext; _passwordService = passwordService; + _userPermissionService = userPermissionService; } private void CheckUsernameFormat(string username, string? paramName) @@ -186,13 +145,15 @@ namespace Timeline.Services throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict); } - private static User CreateUserFromEntity(UserEntity entity) + private async Task CreateUserFromEntity(UserEntity entity) { + var permission = await _userPermissionService.GetPermissionsOfUserAsync(entity.Id); return new User { UniqueId = entity.UniqueId, Username = entity.Username, - Administrator = UserRoleConvert.ToBool(entity.Roles), + Administrator = permission.Contains(UserPermission.UserManagement), + Permissions = permission, Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, Id = entity.Id, Version = entity.Version, @@ -220,32 +181,17 @@ namespace Timeline.Services if (!_passwordService.VerifyPassword(entity.Password, password)) throw new BadPasswordException(password); - return CreateUserFromEntity(entity); + return await CreateUserFromEntity(entity); } - public async Task GetUserById(long id) + public async Task GetUser(long id) { var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(id); - return CreateUserFromEntity(user); - } - - public async Task GetUserByUsername(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - CheckUsernameFormat(username, nameof(username)); - - var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - - return CreateUserFromEntity(entity); + return await CreateUserFromEntity(user); } public async Task GetUserIdByUsername(string username) @@ -263,78 +209,69 @@ namespace Timeline.Services return entity.Id; } - public async Task GetUsers() + public async Task> GetUsers() { - var entities = await _databaseContext.Users.ToArrayAsync(); - return entities.Select(user => CreateUserFromEntity(user)).ToArray(); + List result = new(); + foreach (var entity in await _databaseContext.Users.ToArrayAsync()) + { + result.Add(await CreateUserFromEntity(entity)); + } + return result; } - public async Task CreateUser(User info) + public async Task CreateUser(string username, string password) { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - if (info.Username == null) - throw new ArgumentException(ExceptionUsernameNull, nameof(info)); - CheckUsernameFormat(info.Username, nameof(info)); - - if (info.Password == null) - throw new ArgumentException(ExceptionPasswordNull, nameof(info)); - CheckPasswordFormat(info.Password, nameof(info)); - - if (info.Nickname != null) - CheckNicknameFormat(info.Nickname, nameof(info)); + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); - var username = info.Username; + CheckUsernameFormat(username, nameof(username)); + CheckPasswordFormat(password, nameof(password)); var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); if (conflict) ThrowUsernameConflict(); - var administrator = info.Administrator ?? false; - var password = info.Password; - var nickname = info.Nickname; - var newEntity = new UserEntity { Username = username, Password = _passwordService.HashPassword(password), - Roles = UserRoleConvert.ToString(administrator), - Nickname = nickname, + Roles = "", Version = 1 }; _databaseContext.Users.Add(newEntity); await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(LogDatabaseCreate, - ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); + _logger.LogInformation(Log.Format(LogDatabaseCreate, ("Id", newEntity.Id), ("Username", username))); - return CreateUserFromEntity(newEntity); + return await CreateUserFromEntity(newEntity); } - private void ValidateModifyUserInfo(User? info) + public async Task ModifyUser(long id, ModifyUserParams? param) { - if (info != null) + if (param != null) { - if (info.Username != null) - CheckUsernameFormat(info.Username, nameof(info)); + if (param.Username != null) + CheckUsernameFormat(param.Username, nameof(param)); - if (info.Password != null) - CheckPasswordFormat(info.Password, nameof(info)); + if (param.Password != null) + CheckPasswordFormat(param.Password, nameof(param)); - if (info.Nickname != null) - CheckNicknameFormat(info.Nickname, nameof(info)); + if (param.Nickname != null) + CheckNicknameFormat(param.Nickname, nameof(param)); } - } - private async Task UpdateUserEntity(UserEntity entity, User? info) - { - if (info != null) + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + if (entity == null) + throw new UserNotExistException(id); + + if (param != null) { var now = _clock.GetCurrentTime(); bool updateLastModified = false; - var username = info.Username; + var username = param.Username; if (username != null && username != entity.Username) { var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); @@ -346,21 +283,14 @@ namespace Timeline.Services updateLastModified = true; } - var password = info.Password; + var password = param.Password; if (password != null) { entity.Password = _passwordService.HashPassword(password); entity.Version += 1; } - var administrator = info.Administrator; - if (administrator.HasValue && UserRoleConvert.ToBool(entity.Roles) != administrator) - { - entity.Roles = UserRoleConvert.ToString(administrator.Value); - updateLastModified = true; - } - - var nickname = info.Nickname; + var nickname = param.Nickname; if (nickname != null && nickname != entity.Nickname) { entity.Nickname = nickname; @@ -371,44 +301,12 @@ namespace Timeline.Services { entity.LastModified = now; } - } - } + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); + } - public async Task ModifyUser(long id, User? info) - { - ValidateModifyUserInfo(info); - - var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - if (entity == null) - throw new UserNotExistException(id); - - await UpdateUserEntity(entity, info); - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); - - return CreateUserFromEntity(entity); - } - - public async Task ModifyUser(string username, User? info) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - CheckUsernameFormat(username, nameof(username)); - - ValidateModifyUserInfo(info); - - var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - if (entity == null) - throw new UserNotExistException(username); - - await UpdateUserEntity(entity, info); - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(LogDatabaseUpdate, ("Username", username)); - - return CreateUserFromEntity(entity); + return await CreateUserFromEntity(entity); } public async Task ChangePassword(long id, string oldPassword, string newPassword) diff --git a/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs index 813dae67..09ecd19c 100644 --- a/BackEnd/Timeline/Services/UserTokenManager.cs +++ b/BackEnd/Timeline/Services/UserTokenManager.cs @@ -66,7 +66,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(password)); var user = await _userService.VerifyCredential(username, password); - var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id!.Value, Version = user.Version!.Value, ExpireAt = expireAt }); + var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt }); return new UserTokenCreateResult { Token = token, User = user }; } @@ -86,10 +86,10 @@ namespace Timeline.Services throw new UserTokenTimeExpireException(token, tokenInfo.ExpireAt.Value, currentTime); } - var user = await _userService.GetUserById(tokenInfo.Id); + var user = await _userService.GetUser(tokenInfo.Id); if (tokenInfo.Version < user.Version) - throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version.Value); + throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version); return user; } diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 576836eb..532c63d0 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; @@ -72,6 +73,8 @@ namespace Timeline .AddScheme(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { }); services.AddAuthorization(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); @@ -85,6 +88,7 @@ namespace Timeline services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); -- cgit v1.2.3 From a033b5b1511890f09faf3cdec59763aa8618e617 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 13 Nov 2020 16:15:48 +0800 Subject: refactor(database): Remove roles from user table. --- BackEnd/Timeline/Entities/UserEntity.cs | 3 - .../20201113081411_RemoveRolesFromUser.Designer.cs | 402 +++++++++++++++++++++ .../20201113081411_RemoveRolesFromUser.cs | 24 ++ .../Migrations/DatabaseContextModelSnapshot.cs | 5 - BackEnd/Timeline/Services/UserService.cs | 1 - 5 files changed, 426 insertions(+), 9 deletions(-) create mode 100644 BackEnd/Timeline/Migrations/20201113081411_RemoveRolesFromUser.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20201113081411_RemoveRolesFromUser.cs (limited to 'BackEnd/Timeline/Entities/UserEntity.cs') diff --git a/BackEnd/Timeline/Entities/UserEntity.cs b/BackEnd/Timeline/Entities/UserEntity.cs index 83fa8f89..6a256a31 100644 --- a/BackEnd/Timeline/Entities/UserEntity.cs +++ b/BackEnd/Timeline/Entities/UserEntity.cs @@ -23,9 +23,6 @@ namespace Timeline.Entities [Column("password"), Required] public string Password { get; set; } = default!; - [Column("roles"), Required] - public string Roles { get; set; } = default!; - [Column("version"), Required] public long Version { get; set; } diff --git a/BackEnd/Timeline/Migrations/20201113081411_RemoveRolesFromUser.Designer.cs b/BackEnd/Timeline/Migrations/20201113081411_RemoveRolesFromUser.Designer.cs new file mode 100644 index 00000000..324738a5 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20201113081411_RemoveRolesFromUser.Designer.cs @@ -0,0 +1,402 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20201113081411_RemoveRolesFromUser")] + partial class RemoveRolesFromUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Ref") + .HasColumnType("INTEGER") + .HasColumnName("ref"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tag"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Key") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("key"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateTime") + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("CurrentPostLocalId") + .HasColumnType("INTEGER") + .HasColumnName("current_post_local_id"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("LastModified") + .HasColumnType("TEXT") + .HasColumnName("last_modified"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("NameLastModified") + .HasColumnType("TEXT") + .HasColumnName("name_last_modified"); + + b.Property("OwnerId") + .HasColumnType("INTEGER") + .HasColumnName("owner"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("unique_id") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnType("INTEGER") + .HasColumnName("visibility"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("INTEGER") + .HasColumnName("author"); + + b.Property("Content") + .HasColumnType("TEXT") + .HasColumnName("content"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("content_type"); + + b.Property("ExtraContent") + .HasColumnType("TEXT") + .HasColumnName("extra_content"); + + b.Property("LastUpdated") + .HasColumnType("TEXT") + .HasColumnName("last_updated"); + + b.Property("LocalId") + .HasColumnType("INTEGER") + .HasColumnName("local_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("DataTag") + .HasColumnType("TEXT") + .HasColumnName("data_tag"); + + b.Property("LastModified") + .HasColumnType("TEXT") + .HasColumnName("last_modified"); + + b.Property("Type") + .HasColumnType("TEXT") + .HasColumnName("type"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("create_time") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("last_modified") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnType("TEXT") + .HasColumnName("nickname"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("password"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("unique_id") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("username_change_time") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0L) + .HasColumnName("version"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Permission") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("permission"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_permission"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Timeline"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Timeline"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Navigation("Members"); + + b.Navigation("Posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Navigation("Avatar"); + + b.Navigation("Permissions"); + + b.Navigation("TimelinePosts"); + + b.Navigation("Timelines"); + + b.Navigation("TimelinesJoined"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20201113081411_RemoveRolesFromUser.cs b/BackEnd/Timeline/Migrations/20201113081411_RemoveRolesFromUser.cs new file mode 100644 index 00000000..33d79b33 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20201113081411_RemoveRolesFromUser.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class RemoveRolesFromUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "roles", + table: "users"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "roles", + table: "users", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 95bb0ff5..2f0f75a2 100644 --- a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -251,11 +251,6 @@ namespace Timeline.Migrations .HasColumnType("TEXT") .HasColumnName("password"); - b.Property("Roles") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("roles"); - b.Property("UniqueId") .IsRequired() .ValueGeneratedOnAdd() diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 915c9460..f83d2928 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -242,7 +242,6 @@ namespace Timeline.Services { Username = username, Password = _passwordService.HashPassword(password), - Roles = "", Version = 1 }; _databaseContext.Users.Add(newEntity); -- cgit v1.2.3