Tuitio refactoring

master
Tudor Stanciu 2023-03-07 19:36:12 +02:00
parent e5854ef76b
commit 747b91898d
26 changed files with 117 additions and 104 deletions

View File

@ -17,7 +17,7 @@
<Note>
<Version>1.0.1</Version>
<Content>
Hard changes in token structure. Now the token format is base64 and contains a json with all user data like username, first name, last name, profile picture url, email address and a list of claims that can be configured from the database for each user independently.
Big changes in token structure. Now the token format is base64 and contains a json with all user data like username, first name, last name, profile picture url, email address and a list of claims that can be configured from the database for each user independently.
◾ The generation and validation mechanism for the token has been rewritten to meet the new token structure.
◾ The complexity of user information has grown a lot. All users have now besides the data from token other information such as statuses, failed login attempts, last login date, password change date and security stamp.
◾ All tokens are persisted in the database and the active ones are reload at a server failure or in case of a restart.
@ -60,4 +60,12 @@
◾ Added README.md file
</Content>
</Note>
<Note>
<Version>2.1.0</Version>
<Content>
◾ Tuitio refactoring
◾ Added account logout method
◾ Tuitio performance optimizations
</Content>
</Note>
</ReleaseNotes>

View File

@ -4,7 +4,7 @@ using MediatR;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using Tuitio.Application.Services;
using Tuitio.Application.Services.Abstractions;
using Tuitio.PublishedLanguage.Constants;
using Tuitio.PublishedLanguage.Dto;
@ -30,15 +30,13 @@ namespace Tuitio.Application.CommandHandlers
var loginResult = await _userService.Login(command.UserName, command.Password);
if (loginResult == null)
{
_logger.LogDebug($"Authentication failed for user '{command.UserName}'.");
_logger.LogDebug($"Login failed for user '{command.UserName}'.");
return Envelope<AccountLoginResult>.Error(EnvelopeStatus.BAD_CREDENTIALS);
}
_logger.LogDebug($"Authentication succeeded for user '{command.UserName}'.");
_logger.LogDebug($"Login succeeded for user '{command.UserName}'.");
var token = new Token(loginResult.Raw, loginResult.Token.ExpiresIn);
var result = new AccountLoginResult(token);
var result = new AccountLoginResult(loginResult.Raw, loginResult.Token.ExpiresIn);
return Envelope<AccountLoginResult>.Success(result);
}
}

View File

@ -5,7 +5,8 @@ using MediatR;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using Tuitio.Application.Services;
using Tuitio.Application.Services.Abstractions;
using Tuitio.PublishedLanguage.Constants;
using Tuitio.PublishedLanguage.Dto;
namespace Tuitio.Application.CommandHandlers
@ -30,6 +31,12 @@ namespace Tuitio.Application.CommandHandlers
public async Task<Envelope<AccountLogoutResult>> Handle(Command command, CancellationToken cancellationToken)
{
var logoutResult = await _userService.Logout(command.Token);
if (logoutResult == null)
{
_logger.LogDebug($"Logout failed for token '{command.Token}'.");
return Envelope<AccountLogoutResult>.Error(EnvelopeStatus.UNAUTHENTICATED);
}
_logger.LogDebug($"Logout succeeded for user '{logoutResult.UserName}'.");
var result = _mapper.Map<AccountLogoutResult>(logoutResult);

View File

@ -1,46 +1,46 @@
// Copyright (c) 2020 Tudor Stanciu
using AutoMapper;
using Tuitio.Application.Services;
using Tuitio.PublishedLanguage.Dto;
using MediatR;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using Tuitio.Application.Services.Abstractions;
using Tuitio.PublishedLanguage.Constants;
using Tuitio.PublishedLanguage.Dto;
namespace Tuitio.Application.CommandHandlers
{
public class AuthorizeTokenHandler
public class AuthorizationHandler
{
public record Command(string Token) : IRequest<Envelope<TokenAuthorizationResult>>;
public record Command(string Token) : IRequest<Envelope<AuthorizationResult>>;
public class CommandHandler : IRequestHandler<Command, Envelope<TokenAuthorizationResult>>
public class CommandHandler : IRequestHandler<Command, Envelope<AuthorizationResult>>
{
private readonly IUserService _userService;
private readonly IMapper _mapper;
private readonly ILogger<AuthorizeTokenHandler> _logger;
private readonly ILogger<AuthorizationHandler> _logger;
public CommandHandler(IUserService userService, IMapper mapper, ILogger<AuthorizeTokenHandler> logger)
public CommandHandler(IUserService userService, IMapper mapper, ILogger<AuthorizationHandler> logger)
{
_userService = userService;
_mapper = mapper;
_logger = logger;
}
public Task<Envelope<TokenAuthorizationResult>> Handle(Command command, CancellationToken cancellationToken)
public Task<Envelope<AuthorizationResult>> Handle(Command command, CancellationToken cancellationToken)
{
var token = _userService.Authorize(command.Token);
if (token == null)
{
_logger.LogDebug($"Authorization failed for token '{command.Token}'.");
var result = Envelope<TokenAuthorizationResult>.Error(EnvelopeStatus.UNAUTHORIZED);
var result = Envelope<AuthorizationResult>.Error(EnvelopeStatus.UNAUTHORIZED);
return Task.FromResult(result);
}
_logger.LogDebug($"Authorization succeeded for token '{command.Token}'.");
var authorizationResult = new TokenAuthorizationResult(_mapper.Map<TokenCore>(token));
var envelope = Envelope<TokenAuthorizationResult>.Success(authorizationResult);
var authorizationResult = _mapper.Map<AuthorizationResult>(token);
var envelope = Envelope<AuthorizationResult>.Success(authorizationResult);
return Task.FromResult(envelope);
}
}

View File

@ -1,10 +1,10 @@
// Copyright (c) 2020 Tudor Stanciu
using Microsoft.Extensions.DependencyInjection;
using Tuitio.Application.Services;
using Tuitio.Application.Services.Abstractions;
using Tuitio.Application.Stores;
using Tuitio.Domain.Abstractions;
using Microsoft.Extensions.DependencyInjection;
namespace Tuitio.Application
{

View File

@ -12,7 +12,7 @@ namespace Tuitio.Application.Mappings
{
public MappingProfile()
{
CreateMap<models.Token, dto.TokenCore>();
CreateMap<models.Token, dto.AuthorizationResult>();
CreateMap<AppUser, models.Token>()
.ForMember(z => z.Claims, src => src.MapFrom(z => ComposeClaims(z.Claims)));

View File

@ -3,7 +3,7 @@
using Tuitio.Domain.Entities;
using Tuitio.Domain.Models;
namespace Tuitio.Application.Services
namespace Tuitio.Application.Services.Abstractions
{
internal interface ITokenService
{

View File

@ -1,10 +1,10 @@
// Copyright (c) 2020 Tudor Stanciu
using Tuitio.Domain.Models;
using System.Threading.Tasks;
using Tuitio.Domain.Models;
using Tuitio.Domain.Models.Account;
namespace Tuitio.Application.Services
namespace Tuitio.Application.Services.Abstractions
{
public interface IUserService
{

View File

@ -5,6 +5,7 @@ using Newtonsoft.Json;
using System;
using System.Text;
using System.Text.RegularExpressions;
using Tuitio.Application.Services.Abstractions;
using Tuitio.Application.Stores;
using Tuitio.Domain.Abstractions;
using Tuitio.Domain.Entities;

View File

@ -1,14 +1,14 @@
// Copyright (c) 2020 Tudor Stanciu
using System;
using System.Threading.Tasks;
using Tuitio.Application.Services.Abstractions;
using Tuitio.Application.Stores;
using Tuitio.Domain.Abstractions;
using Tuitio.Domain.Entities;
using Tuitio.Domain.Models;
using Tuitio.Domain.Repositories;
using System;
using System.Threading.Tasks;
using Tuitio.Domain.Models.Account;
using Tuitio.Domain.Repositories;
namespace Tuitio.Application.Services
{
@ -33,6 +33,9 @@ namespace Tuitio.Application.Services
{
var passwordHash = _hashingService.HashSha256(password);
var user = await _userRepository.GetUser(userName, passwordHash);
if (user == null)
return null;
var valid = ValidateUser(user);
if (!valid)
return null;
@ -41,8 +44,7 @@ namespace Tuitio.Application.Services
var raw = _tokenService.GenerateTokenRaw(token);
_securityStore.Set(raw, token);
await _userRepository.UpdateUserAfterAuthentication(user, token, raw);
await _userRepository.UpdateUserAfterLogin(user, token, raw);
var result = new LoginResult(token, raw);
return result;

View File

@ -1,13 +1,13 @@
// Copyright (c) 2020 Tudor Stanciu
using Tuitio.Domain.Data.DbContexts;
using Tuitio.Domain.Entities;
using Tuitio.Domain.Models;
using Tuitio.Domain.Repositories;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
using Tuitio.Domain.Data.DbContexts;
using Tuitio.Domain.Entities;
using Tuitio.Domain.Models;
using Tuitio.Domain.Repositories;
namespace Tuitio.Domain.Data.Repositories
{
@ -28,7 +28,7 @@ namespace Tuitio.Domain.Data.Repositories
.FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password);
}
public async Task UpdateUserAfterAuthentication(AppUser user, Token token, string tokenRaw)
public async Task UpdateUserAfterLogin(AppUser user, Token token, string tokenRaw)
{
var userToken = new UserToken()
{
@ -40,23 +40,25 @@ namespace Tuitio.Domain.Data.Repositories
};
await _dbContext.AddAsync(userToken);
user.LastLoginDate = DateTime.UtcNow;
//asta nu trebuie
//_dbContext.Update(user);
await _dbContext.SaveChangesAsync();
}
public Task<UserToken[]> GetActiveTokens()
public async Task<UserToken[]> GetActiveTokens()
{
var currentDate = DateTime.UtcNow;
// remove expired tokens
_dbContext.UserTokens.RemoveRange(_dbContext.UserTokens.Where(z => z.ValidUntil < currentDate));
await _dbContext.SaveChangesAsync();
// retrieve active tokens
var query = _dbContext.UserTokens
.Where(z => z.ValidFrom <= currentDate && z.ValidUntil >= currentDate);
// read all tokens, keep the valid ones and remove the expired ones.
return query.ToArrayAsync();
var tokens = await query.ToArrayAsync();
return tokens;
}
public Task RemoveToken(Guid tokenId)

View File

@ -10,7 +10,7 @@ namespace Tuitio.Domain.Repositories
public interface IUserRepository
{
Task<AppUser> GetUser(string userName, string password);
Task UpdateUserAfterAuthentication(AppUser user, Token token, string tokenRaw);
Task UpdateUserAfterLogin(AppUser user, Token token, string tokenRaw);
Task<UserToken[]> GetActiveTokens();
Task RemoveToken(Guid tokenId);
}

View File

@ -6,6 +6,7 @@ namespace Tuitio.PublishedLanguage.Constants
{
public const string
BAD_CREDENTIALS = "BAD_CREDENTIALS",
UNAUTHENTICATED = "UNAUTHENTICATED",
UNAUTHORIZED = "UNAUTHORIZED";
}
}

View File

@ -1,10 +1,25 @@
// Copyright (c) 2020 Tudor Stanciu
using System;
using System.Collections.Generic;
namespace Tuitio.PublishedLanguage.Dto
{
public record AccountLoginResult(Token Token);
public record AccountLoginResult(string Token, double ExpiresIn);
public record AccountLogoutResult(int UserId, string UserName, DateTime LogoutDate);
public record TokenAuthorizationResult(TokenCore TokenCore);
public class AuthorizationResult
{
public Guid TokenId { get; init; }
public int UserId { get; init; }
public string UserName { get; init; }
public string FirstName { get; init; }
public string LastName { get; init; }
public string Email { get; init; }
public string ProfilePictureUrl { get; init; }
public string SecurityStamp { get; init; }
public string LockStamp { get; init; }
public DateTime CreatedAt { get; init; }
public double ExpiresIn { get; init; }
public Dictionary<string, string> Claims { get; init; }
}
}

View File

@ -1,7 +0,0 @@
// Copyright (c) 2020 Tudor Stanciu
namespace Tuitio.PublishedLanguage.Dto
{
// move this in result
public record Token(string Raw, double ExpiresIn);
}

View File

@ -1,19 +0,0 @@
// Copyright (c) 2020 Tudor Stanciu
using System.Collections.Generic;
namespace Tuitio.PublishedLanguage.Dto
{
public class TokenCore
{
public int UserId { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string ProfilePictureUrl { get; set; }
public string SecurityStamp { get; set; }
public string LockStamp { get; set; }
public Dictionary<string, string> Claims { get; set; }
}
}

View File

@ -1,3 +1,7 @@
2.0.0 release [2023-01-31 02:17]
2.1.0 release [2023-03-07 22:17]
◾ Tuitio refactoring
◾ Added account logout method
2.0.0 release [2023-01-31 02:17]
◾ Tuitio rebranding
◾ Initial release of Tuitio's published language package

View File

@ -7,7 +7,7 @@
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
<Version>2.0.0</Version>
<Version>2.1.0</Version>
<PackageIcon>logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Company>Toodle HomeLab</Company>

View File

@ -5,7 +5,8 @@ namespace Tuitio.Wrapper.Constants
internal struct ApiRoutes
{
public const string
Authentication = "account/login?UserName={0}&Password={1}",
AccountLogin = "account/login?UserName={0}&Password={1}",
AccountLogout = "account/logout?Token={0}",
Authorization = "connect/authorize?Token={0}";
}
}

View File

@ -1,3 +1,7 @@
2.0.0 release [2023-01-31 02:17]
2.1.0 release [2023-03-07 22:17]
◾ Tuitio refactoring
◾ Added account logout method
2.0.0 release [2023-01-31 02:17]
◾ Tuitio rebranding
◾ Initial release of Tuitio's API wrapper

View File

@ -1,13 +1,13 @@
// Copyright (c) 2020 Tudor Stanciu
using Tuitio.PublishedLanguage.Dto;
using System.Threading.Tasks;
using Tuitio.PublishedLanguage.Dto;
namespace Tuitio.Wrapper.Services
{
public interface ITuitioService
{
Task<Token> Authenticate(string userName, string password);
Task<TokenAuthorizationResult> Authorize(string token);
Task<AccountLoginResult> Login(string userName, string password);
Task<AuthorizationResult> Authorize(string token);
}
}

View File

@ -1,13 +1,13 @@
// Copyright (c) 2020 Tudor Stanciu
using Tuitio.PublishedLanguage.Dto;
using Tuitio.Wrapper.Constants;
using Tuitio.Wrapper.Models;
using Netmash.Extensions.Http;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Tuitio.PublishedLanguage.Dto;
using Tuitio.Wrapper.Constants;
using Tuitio.Wrapper.Models;
namespace Tuitio.Wrapper.Services
{
@ -24,17 +24,24 @@ namespace Tuitio.Wrapper.Services
_httpClient = httpClient;
}
public async Task<Token> Authenticate(string userName, string password)
public async Task<AccountLoginResult> Login(string userName, string password)
{
var route = string.Format(ApiRoutes.Authentication, userName, password);
var result = await _httpClient.ExecutePostRequest<Token>(route);
var route = string.Format(ApiRoutes.AccountLogin, userName, password);
var result = await _httpClient.ExecutePostRequest<AccountLoginResult>(route);
return result;
}
public async Task<TokenAuthorizationResult> Authorize(string token)
public async Task<AccountLogoutResult> Logout(string token)
{
var route = string.Format(ApiRoutes.AccountLogout, token);
var result = await _httpClient.ExecutePostRequest<AccountLogoutResult>(route);
return result;
}
public async Task<AuthorizationResult> Authorize(string token)
{
var route = string.Format(ApiRoutes.Authorization, token);
var result = await _httpClient.ExecutePostRequest<TokenAuthorizationResult>(route);
var result = await _httpClient.ExecutePostRequest<AuthorizationResult>(route);
return result;
}
}

View File

@ -7,7 +7,7 @@
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
<Version>2.0.0</Version>
<Version>2.1.0</Version>
<PackageIcon>logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Company>Toodle HomeLab</Company>

View File

@ -1,4 +1,6 @@
using MediatR;
// Copyright (c) 2020 Tudor Stanciu
using MediatR;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Tuitio.Application.CommandHandlers;
@ -27,12 +29,9 @@ namespace Tuitio.Controllers
[HttpPost("logout")]
public async Task<IActionResult> Logout([FromQuery] string token)
{
var result = await _mediator.Send(token);
if (result != null)
return Ok(result);
else
return BadRequest();
var command = new AccountLogoutHandler.Command(token);
var result = await _mediator.Send(command);
return Ok(result);
}
}
}

View File

@ -21,7 +21,7 @@ namespace Tuitio.Api.Controllers
[HttpPost("authorize")]
public async Task<IActionResult> AuthorizeToken([FromQuery] string token)
{
var command = new AuthorizeTokenHandler.Command(token);
var command = new AuthorizationHandler.Command(token);
var result = await _mediator.Send(command);
return Ok(result);
}

View File

@ -11,11 +11,8 @@ namespace Tuitio.Api.Controllers
[Route("system")]
public class SystemController : ControllerBase
{
private readonly IMediator _mediator;
public SystemController(IMediator mediator)
{
_mediator = mediator;
}
[AllowAnonymous]
@ -24,12 +21,5 @@ namespace Tuitio.Api.Controllers
{
return Ok($"Ping success. System datetime: {DateTime.Now}");
}
/*
Methods:
/version
/burn-token
/burn-all-tokens
*/
}
}