Compare commits
31 Commits
874baa02e8
...
8b7e4b1e64
Author | SHA1 | Date |
---|---|---|
Tudor Stanciu | 8b7e4b1e64 | |
Tudor Stanciu | eefa23b3b8 | |
Tudor Stanciu | 74d68f2329 | |
Tudor Stanciu | fdb08acd21 | |
Tudor Stanciu | ebb0f4de62 | |
Tudor Stanciu | d85ed53cdf | |
Tudor Stanciu | 273f356e5f | |
Tudor Stanciu | 657b9b5204 | |
Tudor Stanciu | 286bebdf36 | |
Tudor Stanciu | 40539b3a69 | |
Tudor Stanciu | 7d7bc9e82f | |
Tudor Stanciu | 55a9cc002d | |
Tudor Stanciu | 54679b8442 | |
Tudor Stanciu | 8e74ec7dc7 | |
Tudor Stanciu | 535621e172 | |
Tudor Stanciu | 0873529b98 | |
Tudor Stanciu | 60ee63d0f5 | |
Tudor Stanciu | 64593dbee3 | |
Tudor Stanciu | 7cca9d9ddf | |
Tudor Stanciu | 9df140e7c0 | |
Tudor Stanciu | 652e6bc142 | |
Tudor Stanciu | 25baaa0a67 | |
Tudor Stanciu | 3f0412e7c5 | |
Tudor Stanciu | 919ab053bc | |
Tudor Stanciu | 8a1ae180ad | |
Tudor Stanciu | 7a88b3c43a | |
Tudor Stanciu | 13ca541478 | |
Tudor Stanciu | 8b5d693201 | |
Tudor Stanciu | 476481f808 | |
Tudor Stanciu | 747b91898d | |
Tudor Stanciu | e5854ef76b |
|
@ -1,7 +1,7 @@
|
||||||
<Project>
|
<Project>
|
||||||
<Import Project="dependencies.props" />
|
<Import Project="dependencies.props" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>2.0.0</Version>
|
<Version>2.4.3</Version>
|
||||||
<Authors>Tudor Stanciu</Authors>
|
<Authors>Tudor Stanciu</Authors>
|
||||||
<Company>STA</Company>
|
<Company>STA</Company>
|
||||||
<PackageTags>Tuitio</PackageTags>
|
<PackageTags>Tuitio</PackageTags>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
# Tuitio
|
# Tuitio
|
||||||
|
|
||||||
Tuitio is a simple identity server implementation focused strictly on the needs of my home lab.
|
Tuitio is a simple identity server implementation focused strictly on the needs of my home lab.
|
||||||
At the moment it has a simple API consisting of only two methods:
|
At the moment it has a simple API consisting of only three methods:
|
||||||
* ```/identity/authenticate``` - handles user authentication using credentials and generates an access token.
|
* ```/account/login``` - handles user authentication using credentials and generates an access token.
|
||||||
* ```/identity/authorize``` - manages the authorization process for a token, including verification of its existence, validity, and authenticity.
|
* ```/account/logout``` - handles user logout.
|
||||||
|
* ```/connect/authorize``` - manages the authorization process for a token, including verification of its existence, validity, and authenticity.
|
||||||
|
|
||||||
***Tuitio*** is a latin word that encompasses meanings such as supervision, safeguarding, defense, guard duty, and protection.
|
***Tuitio*** is a latin word that encompasses meanings such as supervision, safeguarding, defense, guard duty, and protection.
|
||||||
|
|
||||||
|
|
|
@ -8,16 +8,16 @@
|
||||||
A client/consumer can do only two things:
|
A client/consumer can do only two things:
|
||||||
- Authentication: An user name and a password are required in the request body. The request type is POST. The output is an object with the following structure: { token: { raw: "***", validFrom: "", validUntil: "" }, status: "SUCCESS" }
|
- Authentication: An user name and a password are required in the request body. The request type is POST. The output is an object with the following structure: { token: { raw: "***", validFrom: "", validUntil: "" }, status: "SUCCESS" }
|
||||||
- Authorization: The request type is also POST and and its scope is to authorize a token. The input is just the token in string format: { token: "***" }
|
- Authorization: The request type is also POST and and its scope is to authorize a token. The input is just the token in string format: { token: "***" }
|
||||||
For .NET consumers there are two nuget packages developed to facilitate the integration with this identity server:
|
For .NET consumers there are two nuget packages developed to facilitate the integration with this Tuitio server:
|
||||||
- Tuitio.PublishedLanguage: It contains constants and classes for data transfer objects.
|
- Tuitio.PublishedLanguage: It contains constants and classes for data transfer objects.
|
||||||
- Tuitio.Wrapper: It compose and executes all the REST requests to the identity server and offers to a consumer a simple interface with all methods. This interface can be injected with dependency injection at consumer startup with UseIdentityServices method. The only input is the server base address.
|
- Tuitio.Wrapper: It compose and executes all the REST requests to the Tuitio server and offers to a consumer a simple interface with all methods. This interface can be injected with dependency injection at consumer startup with UseTuitioServices method. The only input is the server base address.
|
||||||
- The source of this nugets is public, but on my personal server: https://lab.code-rove.com/public-nuget-server/nuget
|
- The source of this nugets is public, but on my personal server: https://lab.code-rove.com/public-nuget-server/nuget
|
||||||
</Content>
|
</Content>
|
||||||
</Note>
|
</Note>
|
||||||
<Note>
|
<Note>
|
||||||
<Version>1.0.1</Version>
|
<Version>1.0.1</Version>
|
||||||
<Content>
|
<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 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.
|
◾ 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.
|
◾ 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,67 @@
|
||||||
◾ Added README.md file
|
◾ Added README.md file
|
||||||
</Content>
|
</Content>
|
||||||
</Note>
|
</Note>
|
||||||
|
<Note>
|
||||||
|
<Version>2.1.0</Version>
|
||||||
|
<Content>
|
||||||
|
◾ Tuitio refactoring
|
||||||
|
◾ Added account logout method
|
||||||
|
◾ Tuitio performance optimizations
|
||||||
|
</Content>
|
||||||
|
</Note>
|
||||||
|
<Note>
|
||||||
|
<Version>2.2.0</Version>
|
||||||
|
<Content>
|
||||||
|
◾ Added unit testing with xunit
|
||||||
|
◾ Added some tests
|
||||||
|
</Content>
|
||||||
|
</Note>
|
||||||
|
<Note>
|
||||||
|
<Version>2.3.0</Version>
|
||||||
|
<Date>2023-03-27 19:20</Date>
|
||||||
|
<Content>
|
||||||
|
Added "user-info" method in API
|
||||||
|
◾ The "user-info" method returns the data of the authenticated user.
|
||||||
|
◾ Added http context accessor and authentication handler
|
||||||
|
◾ Added user contact options
|
||||||
|
◾ Published new versions of Tuitio's nuget packages
|
||||||
|
</Content>
|
||||||
|
</Note>
|
||||||
|
<Note>
|
||||||
|
<Version>2.4.0</Version>
|
||||||
|
<Date>2023-04-03 01:14</Date>
|
||||||
|
<Content>
|
||||||
|
Added user groups and roles
|
||||||
|
◾ From this version, any user can be assigned to groups and can have roles.
|
||||||
|
◾ Each user group can have roles that will be applied to all users who are part of the group.
|
||||||
|
</Content>
|
||||||
|
</Note>
|
||||||
|
<Note>
|
||||||
|
<Version>2.4.1</Version>
|
||||||
|
<Date>2023-04-07 19:12</Date>
|
||||||
|
<Content>
|
||||||
|
Authentication handler changes
|
||||||
|
◾ The authentication handler has been updated to skip the token validation if the method from controller is marked with [AllowAnonymous] attribute.
|
||||||
|
</Content>
|
||||||
|
</Note>
|
||||||
|
<Note>
|
||||||
|
<Version>2.4.2</Version>
|
||||||
|
<Date>2023-04-08 01:48</Date>
|
||||||
|
<Content>
|
||||||
|
Added user roles and groups in authorization result
|
||||||
|
◾ The authorization result will contain the user role and group codes. They are very useful for an application because after the token is authorized, the application can directly validate its internal permissions based on roles or groups, without calling another method to obtain this information.
|
||||||
|
◾ In addition to these changes, some refactorings were also made.
|
||||||
|
◾ The token "expires in" information measuring unit was changed from milliseconds to seconds.
|
||||||
|
◾ New versions of nuget packages have been released.
|
||||||
|
</Content>
|
||||||
|
</Note>
|
||||||
|
<Note>
|
||||||
|
<Version>2.4.3</Version>
|
||||||
|
<Date>2023-04-12 20:37</Date>
|
||||||
|
<Content>
|
||||||
|
Added IDs for user roles and groups in authorization result
|
||||||
|
◾ Based on the development from the previous version, the authorization result has been extended and will contain both IDs and codes for the groups and roles of the authenticated user.
|
||||||
|
◾ New versions of nuget packages have been released.
|
||||||
|
</Content>
|
||||||
|
</Note>
|
||||||
</ReleaseNotes>
|
</ReleaseNotes>
|
19
Tuitio.sln
19
Tuitio.sln
|
@ -30,6 +30,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tuitio.PublishedLanguage",
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tuitio.Wrapper", "src\Tuitio.Wrapper\Tuitio.Wrapper.csproj", "{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tuitio.Wrapper", "src\Tuitio.Wrapper\Tuitio.Wrapper.csproj", "{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{063D24C5-0823-48D6-A5DD-974CE38F8E71}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{ECDF52C7-A2AA-4070-8FCF-A79A58CFA5F6}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tuitio.Application.Tests", "test\UnitTests\Tuitio.Application.Tests\Tuitio.Application.Tests.csproj", "{11F38E16-13BA-43FB-89FD-CD863FF06BA8}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tuitio.Wrapper.Tests", "test\UnitTests\Tuitio.Wrapper.Tests\Tuitio.Wrapper.Tests.csproj", "{89D54F51-6ABB-4FE2-B060-A60826B6DE1E}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -60,6 +68,14 @@ Global
|
||||||
{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{11F38E16-13BA-43FB-89FD-CD863FF06BA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{11F38E16-13BA-43FB-89FD-CD863FF06BA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{11F38E16-13BA-43FB-89FD-CD863FF06BA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{11F38E16-13BA-43FB-89FD-CD863FF06BA8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{89D54F51-6ABB-4FE2-B060-A60826B6DE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{89D54F51-6ABB-4FE2-B060-A60826B6DE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{89D54F51-6ABB-4FE2-B060-A60826B6DE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{89D54F51-6ABB-4FE2-B060-A60826B6DE1E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -71,6 +87,9 @@ Global
|
||||||
{CE81A435-49AC-4544-A381-FAC91BEB3C49} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C}
|
{CE81A435-49AC-4544-A381-FAC91BEB3C49} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C}
|
||||||
{67B4D1FF-D02E-4DA6-9FB8-F71667360448} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C}
|
{67B4D1FF-D02E-4DA6-9FB8-F71667360448} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C}
|
||||||
{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C}
|
{F6FEC33B-C79E-4484-B356-9C7F1A5E5D95} = {5A8FF505-3E4D-4258-BC3E-CACD74A7B98C}
|
||||||
|
{ECDF52C7-A2AA-4070-8FCF-A79A58CFA5F6} = {063D24C5-0823-48D6-A5DD-974CE38F8E71}
|
||||||
|
{11F38E16-13BA-43FB-89FD-CD863FF06BA8} = {ECDF52C7-A2AA-4070-8FCF-A79A58CFA5F6}
|
||||||
|
{89D54F51-6ABB-4FE2-B060-A60826B6DE1E} = {ECDF52C7-A2AA-4070-8FCF-A79A58CFA5F6}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E93DC46D-9C55-4A05-B299-497CDD90747E}
|
SolutionGuid = {E93DC46D-9C55-4A05-B299-497CDD90747E}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<MediatRPackageVersion>9.0.0</MediatRPackageVersion>
|
<MediatRPackageVersion>9.0.0</MediatRPackageVersion>
|
||||||
<EntityFrameworkCorePackageVersion>6.0.1</EntityFrameworkCorePackageVersion>
|
<EntityFrameworkCorePackageVersion>6.0.1</EntityFrameworkCorePackageVersion>
|
||||||
<NewtonsoftJsonPackageVersion>13.0.1</NewtonsoftJsonPackageVersion>
|
<NewtonsoftJsonPackageVersion>13.0.1</NewtonsoftJsonPackageVersion>
|
||||||
<NetmashExtensionsSwaggerPackageVersion>1.0.6</NetmashExtensionsSwaggerPackageVersion>
|
<NetmashExtensionsSwaggerPackageVersion>1.0.7</NetmashExtensionsSwaggerPackageVersion>
|
||||||
<NetmashDatabaseMigrationPackageVersion>1.2.0</NetmashDatabaseMigrationPackageVersion>
|
<NetmashDatabaseMigrationPackageVersion>1.2.0</NetmashDatabaseMigrationPackageVersion>
|
||||||
<NetmashExtensionsHttpPackageVersion>1.0.0</NetmashExtensionsHttpPackageVersion>
|
<NetmashExtensionsHttpPackageVersion>1.0.0</NetmashExtensionsHttpPackageVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Tuitio.Application.Abstractions
|
||||||
|
{
|
||||||
|
public interface IHttpContextService
|
||||||
|
{
|
||||||
|
int GetUserId();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
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 AccountLoginHandler
|
||||||
|
{
|
||||||
|
public record Command(string UserName, string Password) : IRequest<Envelope<AccountLoginResult>>;
|
||||||
|
|
||||||
|
public class CommandHandler : IRequestHandler<Command, Envelope<AccountLoginResult>>
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly ILogger<AccountLoginHandler> _logger;
|
||||||
|
|
||||||
|
public CommandHandler(IUserService userService, ILogger<AccountLoginHandler> logger)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Envelope<AccountLoginResult>> Handle(Command command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var loginResult = await _userService.Login(command.UserName, command.Password);
|
||||||
|
if (loginResult == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Login failed for user '{command.UserName}'.");
|
||||||
|
return Envelope<AccountLoginResult>.Fail(EnvelopeError.BAD_CREDENTIALS);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug($"Login succeeded for user '{command.UserName}'.");
|
||||||
|
|
||||||
|
var result = new AccountLoginResult(loginResult.Raw, loginResult.Token.ExpiresIn);
|
||||||
|
return Envelope<AccountLoginResult>.Success(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using AutoMapper;
|
||||||
|
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 AccountLogoutHandler
|
||||||
|
{
|
||||||
|
public record Command(string Token) : IRequest<Envelope<AccountLogoutResult>>;
|
||||||
|
|
||||||
|
public class CommandHandler : IRequestHandler<Command, Envelope<AccountLogoutResult>>
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly ILogger<AccountLoginHandler> _logger;
|
||||||
|
|
||||||
|
public CommandHandler(IUserService userService, IMapper mapper, ILogger<AccountLoginHandler> logger)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_mapper = mapper;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>.Fail(EnvelopeError.UNAUTHENTICATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug($"Logout succeeded for user '{logoutResult.UserName}'.");
|
||||||
|
|
||||||
|
var result = _mapper.Map<AccountLogoutResult>(logoutResult);
|
||||||
|
return Envelope<AccountLogoutResult>.Success(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,44 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using AutoMapper;
|
|
||||||
using Tuitio.Application.Commands;
|
|
||||||
using Tuitio.Application.Services;
|
|
||||||
using Tuitio.PublishedLanguage.Constants;
|
|
||||||
using Tuitio.PublishedLanguage.Dto;
|
|
||||||
using Tuitio.PublishedLanguage.Events;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.CommandHandlers
|
|
||||||
{
|
|
||||||
public class AuthenticateUserHandler : IRequestHandler<AuthenticateUser, AuthenticateUserResult>
|
|
||||||
{
|
|
||||||
private readonly IUserService _userService;
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
private readonly ILogger<AuthenticateUserHandler> _logger;
|
|
||||||
|
|
||||||
public AuthenticateUserHandler(IUserService userService, IMapper mapper, ILogger<AuthenticateUserHandler> logger)
|
|
||||||
{
|
|
||||||
_userService = userService;
|
|
||||||
_mapper = mapper;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<AuthenticateUserResult> Handle(AuthenticateUser command, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var internalToken = await _userService.Authenticate(command.UserName, command.Password);
|
|
||||||
if (internalToken == null)
|
|
||||||
{
|
|
||||||
_logger.LogDebug($"Authentication failed for user '{command.UserName}'.");
|
|
||||||
return new AuthenticateUserResult() { Status = AuthenticationStatus.BAD_CREDENTIALS };
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug($"Authentication succeeded for user '{command.UserName}'.");
|
|
||||||
var token = _mapper.Map<Token>(internalToken);
|
|
||||||
|
|
||||||
return new AuthenticateUserResult() { Token = token, Status = AuthenticationStatus.SUCCESS };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using AutoMapper;
|
||||||
|
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 AuthorizationHandler
|
||||||
|
{
|
||||||
|
public record Command(string Token) : IRequest<Envelope<AuthorizationResult>>;
|
||||||
|
|
||||||
|
public class CommandHandler : IRequestHandler<Command, Envelope<AuthorizationResult>>
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly ILogger<AuthorizationHandler> _logger;
|
||||||
|
|
||||||
|
public CommandHandler(IUserService userService, IMapper mapper, ILogger<AuthorizationHandler> logger)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_mapper = mapper;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<AuthorizationResult>.Fail(EnvelopeError.UNAUTHORIZED);
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug($"Authorization succeeded for token '{command.Token}'.");
|
||||||
|
var authorizationResult = _mapper.Map<AuthorizationResult>(token);
|
||||||
|
var envelope = Envelope<AuthorizationResult>.Success(authorizationResult);
|
||||||
|
return Task.FromResult(envelope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,42 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using AutoMapper;
|
|
||||||
using Tuitio.Application.Commands;
|
|
||||||
using Tuitio.Application.Services;
|
|
||||||
using Tuitio.PublishedLanguage.Dto;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.CommandHandlers
|
|
||||||
{
|
|
||||||
public class AuthorizeTokenHandler : IRequestHandler<AuthorizeToken, TokenCore>
|
|
||||||
{
|
|
||||||
private readonly IUserService _userService;
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
private readonly ILogger<AuthorizeTokenHandler> _logger;
|
|
||||||
|
|
||||||
public AuthorizeTokenHandler(IUserService userService, IMapper mapper, ILogger<AuthorizeTokenHandler> logger)
|
|
||||||
{
|
|
||||||
_userService = userService;
|
|
||||||
_mapper = mapper;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<TokenCore> Handle(AuthorizeToken command, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var tokenCore = _userService.Authorize(command.Token);
|
|
||||||
if (tokenCore == null)
|
|
||||||
{
|
|
||||||
_logger.LogDebug($"Authorization failed for token '{command.Token}'.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug($"Authorization succeeded for token '{command.Token}'.");
|
|
||||||
var tokenCoreResult = _mapper.Map<TokenCore>(tokenCore);
|
|
||||||
|
|
||||||
return Task.FromResult(tokenCoreResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using MediatR;
|
|
||||||
using Tuitio.PublishedLanguage.Events;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.Commands
|
|
||||||
{
|
|
||||||
public class AuthenticateUser : IRequest<AuthenticateUserResult>
|
|
||||||
{
|
|
||||||
public string UserName { get; set; }
|
|
||||||
public string Password { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using MediatR;
|
|
||||||
using Tuitio.PublishedLanguage.Dto;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.Commands
|
|
||||||
{
|
|
||||||
public class AuthorizeToken : IRequest<TokenCore>
|
|
||||||
{
|
|
||||||
public string Token { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,10 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Tuitio.Application.Services;
|
using Tuitio.Application.Services;
|
||||||
using Tuitio.Application.Services.Abstractions;
|
using Tuitio.Application.Services.Abstractions;
|
||||||
using Tuitio.Application.Stores;
|
using Tuitio.Application.Stores;
|
||||||
using Tuitio.Domain.Abstractions;
|
using Tuitio.Domain.Abstractions;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Tuitio.Application
|
namespace Tuitio.Application
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
using Tuitio.Domain.Models;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Extensions
|
||||||
|
{
|
||||||
|
internal static class EntityConversions
|
||||||
|
{
|
||||||
|
public static Dictionary<string, string> ToDictionary(this ICollection<UserClaim> claims)
|
||||||
|
{
|
||||||
|
if (claims == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var result = new Dictionary<string, string>();
|
||||||
|
foreach (var claim in claims)
|
||||||
|
result.Add(claim.ClaimKey, claim.ClaimValue);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<RecordIdentifier> AsRecordIdentifiers(this IEnumerable<UserGroup> userGroups)
|
||||||
|
{
|
||||||
|
if (userGroups == null)
|
||||||
|
throw new ArgumentNullException(nameof(userGroups));
|
||||||
|
|
||||||
|
var result = userGroups.Select(z => new RecordIdentifier(z.UserGroupId, z.UserGroupCode));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<RecordIdentifier> AsRecordIdentifiers(this IEnumerable<UserRole> userRoles)
|
||||||
|
{
|
||||||
|
if (userRoles == null)
|
||||||
|
throw new ArgumentNullException(nameof(userRoles));
|
||||||
|
|
||||||
|
var result = userRoles.Select(z => new RecordIdentifier(z.UserRoleId, z.UserRoleCode));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Extensions
|
||||||
|
{
|
||||||
|
internal static class EntityExtensions
|
||||||
|
{
|
||||||
|
public static IEnumerable<UserRole> GetUserRoles(this AppUser user)
|
||||||
|
{
|
||||||
|
var roles = new List<UserRole>();
|
||||||
|
|
||||||
|
var groups = user.UserGroups?.Select(z => z.UserGroup);
|
||||||
|
if (groups != null)
|
||||||
|
{
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
var groupRoles = group.GroupRoles?.Select(z => z.UserRole);
|
||||||
|
if (groupRoles == null)
|
||||||
|
continue;
|
||||||
|
roles.AddRange(groupRoles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.UserRoles != null)
|
||||||
|
roles.AddRange(user.UserRoles.Select(z => z.UserRole));
|
||||||
|
|
||||||
|
return roles.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Tuitio.Domain.Entities;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using dto = Tuitio.PublishedLanguage.Dto;
|
using dto = Tuitio.PublishedLanguage.Dto;
|
||||||
using models = Tuitio.Domain.Models;
|
using models = Tuitio.Domain.Models;
|
||||||
|
|
||||||
|
@ -12,22 +10,9 @@ namespace Tuitio.Application.Mappings
|
||||||
{
|
{
|
||||||
public MappingProfile()
|
public MappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<models.Token, dto.Token>();
|
CreateMap<models.RecordIdentifier, dto.RecordIdentifier>();
|
||||||
CreateMap<models.TokenCore, dto.TokenCore>();
|
CreateMap<models.Token, dto.AuthorizationResult>();
|
||||||
CreateMap<AppUser, models.TokenCore>()
|
CreateMap<models.Account.LogoutResult, dto.AccountLogoutResult>();
|
||||||
.ForMember(z => z.Claims, src => src.MapFrom(z => ComposeClaims(z.Claims)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> ComposeClaims(ICollection<UserClaim> claims)
|
|
||||||
{
|
|
||||||
if (claims == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var result = new Dictionary<string, string>();
|
|
||||||
foreach (var claim in claims)
|
|
||||||
result.Add(claim.ClaimKey, claim.ClaimValue);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using AutoMapper;
|
||||||
|
using System.Linq;
|
||||||
|
using Tuitio.Application.Extensions;
|
||||||
|
using Tuitio.Application.Queries;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Mappings
|
||||||
|
{
|
||||||
|
public class UserInfoMappings : Profile
|
||||||
|
{
|
||||||
|
public UserInfoMappings()
|
||||||
|
{
|
||||||
|
CreateMap<AppUser, GetUserInfo.Model>()
|
||||||
|
.ForMember(z => z.Claims, src => src.MapFrom(z => z.Claims.ToDictionary()))
|
||||||
|
.ForMember(z => z.UserGroups, src => src.MapFrom(z => z.UserGroups.Select(z => z.UserGroup)))
|
||||||
|
.ForMember(z => z.UserRoles, src => src.MapFrom(z => z.GetUserRoles()));
|
||||||
|
|
||||||
|
CreateMap<ContactOption, GetUserInfo.ContactOption>()
|
||||||
|
.ForMember(z => z.Id, src => src.MapFrom(z => z.ContactOptionId))
|
||||||
|
.ForMember(z => z.ContactTypeCode, src => src.MapFrom(z => z.ContactType.ContactTypeCode))
|
||||||
|
.ForMember(z => z.ContactTypeName, src => src.MapFrom(z => z.ContactType.ContactTypeName));
|
||||||
|
|
||||||
|
CreateMap<UserGroup, GetUserInfo.UserGroup>()
|
||||||
|
.ForMember(z => z.Id, src => src.MapFrom(z => z.UserGroupId))
|
||||||
|
.ForMember(z => z.Code, src => src.MapFrom(z => z.UserGroupCode))
|
||||||
|
.ForMember(z => z.Name, src => src.MapFrom(z => z.UserGroupName));
|
||||||
|
|
||||||
|
CreateMap<UserRole, GetUserInfo.UserRole>()
|
||||||
|
.ForMember(z => z.Id, src => src.MapFrom(z => z.UserRoleId))
|
||||||
|
.ForMember(z => z.Code, src => src.MapFrom(z => z.UserRoleCode))
|
||||||
|
.ForMember(z => z.Name, src => src.MapFrom(z => z.UserRoleName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Tuitio.Application.Tests")]
|
|
@ -0,0 +1,80 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using AutoMapper;
|
||||||
|
using MediatR;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tuitio.Application.Abstractions;
|
||||||
|
using Tuitio.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Queries
|
||||||
|
{
|
||||||
|
public class GetUserInfo
|
||||||
|
{
|
||||||
|
public class Query : IRequest<Model> { }
|
||||||
|
|
||||||
|
public record Model
|
||||||
|
{
|
||||||
|
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 DateTime CreationDate { get; init; }
|
||||||
|
public int? FailedLoginAttempts { get; init; }
|
||||||
|
public DateTime? LastLoginDate { get; init; }
|
||||||
|
public Dictionary<string, string> Claims { get; init; }
|
||||||
|
public UserRole[] UserRoles { get; init; }
|
||||||
|
public UserGroup[] UserGroups { get; init; }
|
||||||
|
public ContactOption[] ContactOptions { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ContactOption
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string ContactTypeCode { get; init; }
|
||||||
|
public string ContactTypeName { get; init; }
|
||||||
|
public string ContactValue { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UserGroup
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string Code { get; init; }
|
||||||
|
public string Name { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UserRole
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string Code { get; init; }
|
||||||
|
public string Name { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QueryHandler : IRequestHandler<Query, Model>
|
||||||
|
{
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IHttpContextService _httpContextService;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public QueryHandler(IUserRepository userRepository, IHttpContextService httpContextService, IMapper mapper)
|
||||||
|
{
|
||||||
|
_userRepository=userRepository;
|
||||||
|
_httpContextService=httpContextService;
|
||||||
|
_mapper=mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Model> Handle(Query request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = _httpContextService.GetUserId();
|
||||||
|
var user = await _userRepository.GetFullUser(userId);
|
||||||
|
var info = _mapper.Map<Model>(user);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,10 @@
|
||||||
using Tuitio.Domain.Entities;
|
using Tuitio.Domain.Entities;
|
||||||
using Tuitio.Domain.Models;
|
using Tuitio.Domain.Models;
|
||||||
|
|
||||||
namespace Tuitio.Application.Services
|
namespace Tuitio.Application.Services.Abstractions
|
||||||
{
|
{
|
||||||
internal interface ITokenService
|
internal interface ITokenService
|
||||||
{
|
{
|
||||||
Token GenerateToken(AppUser user);
|
Token GenerateToken(AppUser user);
|
||||||
TokenCore ExtractTokenCore(string tokenRaw);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tuitio.Domain.Models;
|
||||||
|
using Tuitio.Domain.Models.Account;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Services.Abstractions
|
||||||
|
{
|
||||||
|
public interface IUserService
|
||||||
|
{
|
||||||
|
Task<LoginResult> Login(string userName, string password);
|
||||||
|
Task<LogoutResult> Logout(string tokenRaw);
|
||||||
|
Token Authorize(string tokenRaw);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Tuitio.Application.Services.Abstractions;
|
using Tuitio.Application.Services.Abstractions;
|
||||||
using Tuitio.Application.Stores;
|
using Tuitio.Application.Stores;
|
||||||
using Tuitio.Domain.Entities;
|
using Tuitio.Domain.Entities;
|
||||||
using Tuitio.Domain.Models;
|
using Tuitio.Domain.Models;
|
||||||
using Tuitio.Domain.Repositories;
|
using Tuitio.Domain.Repositories;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.Services
|
namespace Tuitio.Application.Services
|
||||||
{
|
{
|
||||||
|
@ -33,7 +33,7 @@ namespace Tuitio.Application.Services
|
||||||
var activeTokens = Array.Empty<UserToken>();
|
var activeTokens = Array.Empty<UserToken>();
|
||||||
using (var scope = _serviceProvider.CreateScope())
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
{
|
{
|
||||||
var _repository = scope.ServiceProvider.GetRequiredService<IIdentityRepository>();
|
var _repository = scope.ServiceProvider.GetRequiredService<IUserRepository>();
|
||||||
activeTokens = await _repository.GetActiveTokens();
|
activeTokens = await _repository.GetActiveTokens();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,8 +43,8 @@ namespace Tuitio.Application.Services
|
||||||
_logger.LogInformation($"BehaviorService: {activeTokens.Length} active tokens were found in database.");
|
_logger.LogInformation($"BehaviorService: {activeTokens.Length} active tokens were found in database.");
|
||||||
foreach (var token in activeTokens)
|
foreach (var token in activeTokens)
|
||||||
{
|
{
|
||||||
var storeToken = new Token() { Raw = token.Token, ValidFrom = token.ValidFrom, ValidUntil = token.ValidUntil };
|
var storeToken = Token.Import(token.Token);
|
||||||
_securityStore.SetToken(storeToken, token.UserId);
|
_securityStore.Set(token.Token, storeToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using Tuitio.Domain.Models;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.Services
|
|
||||||
{
|
|
||||||
public interface IUserService
|
|
||||||
{
|
|
||||||
Task<Token> Authenticate(string userName, string password);
|
|
||||||
TokenCore Authorize(string token);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +1,32 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
using AutoMapper;
|
using System.Linq;
|
||||||
|
using Tuitio.Application.Extensions;
|
||||||
|
using Tuitio.Application.Services.Abstractions;
|
||||||
using Tuitio.Domain.Abstractions;
|
using Tuitio.Domain.Abstractions;
|
||||||
using Tuitio.Domain.Entities;
|
using Tuitio.Domain.Entities;
|
||||||
using Tuitio.Domain.Models;
|
using Tuitio.Domain.Models;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.Services
|
namespace Tuitio.Application.Services
|
||||||
{
|
{
|
||||||
internal class TokenService : ITokenService
|
internal class TokenService : ITokenService
|
||||||
{
|
{
|
||||||
private readonly IMapper _mapper;
|
|
||||||
private readonly IConfigProvider _configProvider;
|
private readonly IConfigProvider _configProvider;
|
||||||
|
|
||||||
public TokenService(IMapper mapper, IConfigProvider configProvider)
|
public TokenService(IConfigProvider configProvider)
|
||||||
{
|
{
|
||||||
_mapper = mapper;
|
|
||||||
_configProvider=configProvider;
|
_configProvider=configProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Token GenerateToken(AppUser user)
|
public Token GenerateToken(AppUser user)
|
||||||
{
|
{
|
||||||
var tokenRaw = GenerateTokenRaw(user);
|
var token = new Token(_configProvider.Token.ValidityInMinutes);
|
||||||
var currentDate = DateTime.Now;
|
var claims = user.Claims?.ToDictionary();
|
||||||
var token = new Token() { Raw = tokenRaw, ValidFrom = currentDate, ValidUntil = currentDate.AddMinutes(_configProvider.Token.ValidityInMinutes) };
|
var userRoles = user.GetUserRoles().AsRecordIdentifiers();
|
||||||
|
var userGroups = user.UserGroups?.Select(z => z.UserGroup).AsRecordIdentifiers();
|
||||||
|
|
||||||
|
token.SetUserData(user.UserId, user.UserName, user.FirstName, user.LastName, user.Email, user.SecurityStamp, claims, userRoles, userGroups);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GenerateTokenRaw(AppUser user)
|
|
||||||
{
|
|
||||||
var tokenCore = GenerateTokenCore(user);
|
|
||||||
var tokenCoreString = JsonConvert.SerializeObject(tokenCore);
|
|
||||||
var tokenCoreBytes = Encoding.UTF8.GetBytes(tokenCoreString);
|
|
||||||
var tokenRaw = Convert.ToBase64String(tokenCoreBytes);
|
|
||||||
return tokenRaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TokenCore GenerateTokenCore(AppUser user)
|
|
||||||
{
|
|
||||||
var tokenCore = _mapper.Map<TokenCore>(user);
|
|
||||||
tokenCore.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "");
|
|
||||||
return tokenCore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TokenCore ExtractTokenCore(string tokenRaw)
|
|
||||||
{
|
|
||||||
var valid = ValidateTokenRaw(tokenRaw);
|
|
||||||
if (!valid)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var tokenCoreBytes = Convert.FromBase64String(tokenRaw);
|
|
||||||
var tokenCoreString = Encoding.UTF8.GetString(tokenCoreBytes);
|
|
||||||
var tokenCore = JsonConvert.DeserializeObject<TokenCore>(tokenCoreString);
|
|
||||||
return tokenCore;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateTokenRaw(string tokenRaw)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(tokenRaw))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!StringIsBase64(tokenRaw))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool StringIsBase64(string str)
|
|
||||||
{
|
|
||||||
str = str.Trim();
|
|
||||||
return (str.Length % 4 == 0) && Regex.IsMatch(str, @"^[a-zA-Z0-9+/]*={0,3}$", RegexOptions.None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,89 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Tuitio.Application.Services.Abstractions;
|
using Tuitio.Application.Services.Abstractions;
|
||||||
using Tuitio.Application.Stores;
|
using Tuitio.Application.Stores;
|
||||||
using Tuitio.Domain.Abstractions;
|
using Tuitio.Domain.Abstractions;
|
||||||
using Tuitio.Domain.Entities;
|
using Tuitio.Domain.Entities;
|
||||||
using Tuitio.Domain.Models;
|
using Tuitio.Domain.Models;
|
||||||
|
using Tuitio.Domain.Models.Account;
|
||||||
using Tuitio.Domain.Repositories;
|
using Tuitio.Domain.Repositories;
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.Services
|
namespace Tuitio.Application.Services
|
||||||
{
|
{
|
||||||
internal class UserService : IUserService
|
internal class UserService : IUserService
|
||||||
{
|
{
|
||||||
private readonly ITokenStore _securityStore;
|
private readonly ITokenStore _securityStore;
|
||||||
private readonly IIdentityRepository _identityRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly ITokenService _tokenService;
|
private readonly ITokenService _tokenService;
|
||||||
private readonly IConfigProvider _configProvider;
|
private readonly IConfigProvider _configProvider;
|
||||||
private readonly IHashingService _hashingService;
|
private readonly IHashingService _hashingService;
|
||||||
|
|
||||||
public UserService(ITokenStore securityStore, IIdentityRepository identityRepository, ITokenService tokenService, IConfigProvider configProvider, IHashingService hashingService)
|
public UserService(ITokenStore securityStore, IUserRepository userRepository, ITokenService tokenService, IConfigProvider configProvider, IHashingService hashingService)
|
||||||
{
|
{
|
||||||
_securityStore=securityStore;
|
_securityStore=securityStore;
|
||||||
_identityRepository=identityRepository;
|
_userRepository=userRepository;
|
||||||
_tokenService=tokenService;
|
_tokenService=tokenService;
|
||||||
_configProvider=configProvider;
|
_configProvider=configProvider;
|
||||||
_hashingService=hashingService;
|
_hashingService=hashingService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Token> Authenticate(string userName, string password)
|
public async Task<LoginResult> Login(string userName, string password)
|
||||||
{
|
{
|
||||||
|
ValidateCredentials(userName, password);
|
||||||
|
|
||||||
var passwordHash = _hashingService.HashSha256(password);
|
var passwordHash = _hashingService.HashSha256(password);
|
||||||
var user = await _identityRepository.GetUser(userName, passwordHash);
|
var user = await _userRepository.GetUser(userName, passwordHash);
|
||||||
|
if (user == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
var valid = ValidateUser(user);
|
var valid = ValidateUser(user);
|
||||||
if (!valid)
|
if (!valid)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var token = _tokenService.GenerateToken(user);
|
var token = _tokenService.GenerateToken(user);
|
||||||
_securityStore.SetToken(token, user.UserId);
|
var raw = token.Export();
|
||||||
await _identityRepository.UpdateUserAfterAuthentication(user, token);
|
_securityStore.Set(raw, token);
|
||||||
|
|
||||||
|
await _userRepository.UpdateUserAfterLogin(user, token, raw);
|
||||||
|
|
||||||
|
var result = new LoginResult(token, raw);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LogoutResult> Logout(string tokenRaw)
|
||||||
|
{
|
||||||
|
var token = _securityStore.Get(tokenRaw);
|
||||||
|
if (token == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
await _userRepository.RemoveToken(token.TokenId);
|
||||||
|
_securityStore.Remove(tokenRaw);
|
||||||
|
|
||||||
|
var result = new LogoutResult(token.UserId, token.UserName, DateTime.UtcNow);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Token Authorize(string tokenRaw)
|
||||||
|
{
|
||||||
|
var token = _securityStore.Get(tokenRaw);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TokenCore Authorize(string token)
|
private void ValidateCredentials(string userName, string password)
|
||||||
{
|
{
|
||||||
var tokenCore = _securityStore.ValidateAndGetTokenCore(token);
|
if (string.IsNullOrEmpty(userName))
|
||||||
if (tokenCore == null)
|
throw new ArgumentException($"Value cannot be null or empty string.", nameof(userName));
|
||||||
return null;
|
|
||||||
|
|
||||||
return tokenCore;
|
if (string.IsNullOrEmpty(password))
|
||||||
|
throw new ArgumentException($"Value cannot be null or empty string.", nameof(password));
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ValidateUser(AppUser user)
|
private bool ValidateUser(AppUser user)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return false;
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
|
||||||
if (user.FailedLoginAttempts.HasValue && user.FailedLoginAttempts.Value > _configProvider.Restrictions.MaxFailedLoginAttempts)
|
if (user.FailedLoginAttempts.HasValue && user.FailedLoginAttempts.Value > _configProvider.Restrictions.MaxFailedLoginAttempts)
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System;
|
||||||
using Tuitio.Domain.Models;
|
using Tuitio.Domain.Models;
|
||||||
|
|
||||||
namespace Tuitio.Application.Stores
|
namespace Tuitio.Application.Stores
|
||||||
{
|
{
|
||||||
internal interface ITokenStore
|
internal interface ITokenStore
|
||||||
{
|
{
|
||||||
void SetToken(Token token, int userId);
|
Token Get(string key);
|
||||||
TokenCore ValidateAndGetTokenCore(string token);
|
bool Set(string key, Token token);
|
||||||
|
void Remove(string key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +1,39 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
using Tuitio.Application.Services;
|
|
||||||
using Tuitio.Domain.Models;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using Tuitio.Domain.Models;
|
||||||
|
|
||||||
namespace Tuitio.Application.Stores
|
namespace Tuitio.Application.Stores
|
||||||
{
|
{
|
||||||
internal class TokenStore : ITokenStore
|
internal class TokenStore : ITokenStore
|
||||||
{
|
{
|
||||||
private readonly ITokenService _tokenService;
|
private ConcurrentDictionary<string, Token> Tokens { get; }
|
||||||
private ConcurrentDictionary<int, List<Token>> Tokens { get; }
|
|
||||||
|
|
||||||
public TokenStore(ITokenService tokenService)
|
public TokenStore()
|
||||||
{
|
{
|
||||||
_tokenService = tokenService;
|
Tokens = new ConcurrentDictionary<string, Token>();
|
||||||
Tokens = new ConcurrentDictionary<int, List<Token>>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetToken(Token token, int userId)
|
public Token Get(string key)
|
||||||
{
|
{
|
||||||
var registered = Tokens.TryGetValue(userId, out List<Token> list);
|
var registered = Tokens.ContainsKey(key);
|
||||||
|
|
||||||
if (registered)
|
|
||||||
list.Add(token);
|
|
||||||
else
|
|
||||||
Tokens.TryAdd(userId, new List<Token>() { token });
|
|
||||||
}
|
|
||||||
|
|
||||||
public TokenCore ValidateAndGetTokenCore(string token)
|
|
||||||
{
|
|
||||||
var tokenCore = _tokenService.ExtractTokenCore(token);
|
|
||||||
if (tokenCore == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var registered = Tokens.TryGetValue(tokenCore.UserId, out List<Token> list);
|
|
||||||
if (!registered)
|
if (!registered)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var valid = list.FirstOrDefault(z => z.Raw == token);
|
return Tokens[key];
|
||||||
if (valid == null)
|
}
|
||||||
return null;
|
|
||||||
|
|
||||||
return tokenCore;
|
public bool Set(string key, Token token)
|
||||||
}
|
{
|
||||||
|
var registered = Tokens.ContainsKey(key);
|
||||||
|
if (registered)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Tokens.TryAdd(key, token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key) => Tokens.Remove(key, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
@ -11,7 +11,6 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -6,12 +6,13 @@ using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Tuitio.Domain.Data.DbContexts
|
namespace Tuitio.Domain.Data.DbContexts
|
||||||
{
|
{
|
||||||
public class IdentityDbContext : DbContext
|
public class TuitioDbContext : DbContext
|
||||||
{
|
{
|
||||||
|
public DbSet<UserStatus> UserStatuses{ get; set; }
|
||||||
public DbSet<AppUser> Users { get; set; }
|
public DbSet<AppUser> Users { get; set; }
|
||||||
public DbSet<UserToken> UserTokens { get; set; }
|
public DbSet<UserToken> UserTokens { get; set; }
|
||||||
|
|
||||||
public IdentityDbContext(DbContextOptions<IdentityDbContext> options)
|
public TuitioDbContext(DbContextOptions<TuitioDbContext> options)
|
||||||
: base(options)
|
: base(options)
|
||||||
{
|
{
|
||||||
base.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
|
base.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
|
||||||
|
@ -26,6 +27,13 @@ namespace Tuitio.Domain.Data.DbContexts
|
||||||
modelBuilder.ApplyConfiguration(new AppUserConfiguration());
|
modelBuilder.ApplyConfiguration(new AppUserConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new UserClaimConfiguration());
|
modelBuilder.ApplyConfiguration(new UserClaimConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new UserTokenConfiguration());
|
modelBuilder.ApplyConfiguration(new UserTokenConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new ContactTypeConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new ContactOptionConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new UserGroupConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new UserRoleConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new UserXUserGroupConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new UserGroupXUserRoleConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new UserXUserRoleConfiguration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,22 +1,26 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Tuitio.Domain.Data.DbContexts;
|
using Tuitio.Domain.Data.DbContexts;
|
||||||
using Tuitio.Domain.Data.Repositories;
|
using Tuitio.Domain.Data.Repositories;
|
||||||
using Tuitio.Domain.Repositories;
|
using Tuitio.Domain.Repositories;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Tuitio.Domain.Data
|
namespace Tuitio.Domain.Data
|
||||||
{
|
{
|
||||||
public static class DependencyInjectionExtensions
|
public static class DependencyInjectionExtensions
|
||||||
{
|
{
|
||||||
|
public static void AddDataAccessServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IUserRepository, UserRepository>();
|
||||||
|
}
|
||||||
|
|
||||||
public static void AddDataAccess(this IServiceCollection services)
|
public static void AddDataAccess(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IIdentityRepository, IdentityRepository>();
|
services.AddDataAccessServices();
|
||||||
|
|
||||||
services
|
services
|
||||||
.AddDbContextPool<IdentityDbContext>(
|
.AddDbContextPool<TuitioDbContext>(
|
||||||
(serviceProvider, options) =>
|
(serviceProvider, options) =>
|
||||||
{
|
{
|
||||||
var configuration = serviceProvider.GetService<IConfiguration>();
|
var configuration = serviceProvider.GetService<IConfiguration>();
|
||||||
|
|
|
@ -14,6 +14,9 @@ namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
builder.Property(z => z.UserId).ValueGeneratedOnAdd();
|
builder.Property(z => z.UserId).ValueGeneratedOnAdd();
|
||||||
builder.HasOne(z => z.Status).WithMany().HasForeignKey(z => z.StatusId);
|
builder.HasOne(z => z.Status).WithMany().HasForeignKey(z => z.StatusId);
|
||||||
builder.HasMany(z => z.Claims).WithOne().HasForeignKey(z => z.UserId);
|
builder.HasMany(z => z.Claims).WithOne().HasForeignKey(z => z.UserId);
|
||||||
|
builder.HasMany(z => z.ContactOptions).WithOne().HasForeignKey(z => z.UserId);
|
||||||
|
builder.HasMany(z => z.UserGroups).WithOne().HasForeignKey(z => z.UserId);
|
||||||
|
builder.HasMany(z => z.UserRoles).WithOne().HasForeignKey(z => z.UserId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
|
{
|
||||||
|
class ContactOptionConfiguration : IEntityTypeConfiguration<ContactOption>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ContactOption> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("ContactOption").HasKey(key => key.ContactOptionId);
|
||||||
|
builder.Property(z => z.ContactOptionId).ValueGeneratedOnAdd();
|
||||||
|
builder.HasOne(z => z.ContactType).WithMany().HasForeignKey(z => z.ContactTypeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
|
{
|
||||||
|
class ContactTypeConfiguration : IEntityTypeConfiguration<ContactType>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ContactType> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("ContactType").HasKey(z => z.ContactTypeId);
|
||||||
|
builder.Property(z => z.ContactTypeId).ValueGeneratedOnAdd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
|
{
|
||||||
|
class UserGroupConfiguration : IEntityTypeConfiguration<UserGroup>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserGroup> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("UserGroup").HasKey(z => z.UserGroupId);
|
||||||
|
builder.Property(z => z.UserGroupId).ValueGeneratedOnAdd();
|
||||||
|
builder.HasMany(z => z.GroupRoles).WithOne().HasForeignKey(z => z.UserGroupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
|
{
|
||||||
|
class UserGroupXUserRoleConfiguration : IEntityTypeConfiguration<UserGroupXUserRole>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserGroupXUserRole> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("UserGroupXUserRole").HasKey(z => new { z.UserGroupId, z.UserRoleId });
|
||||||
|
builder.HasOne(z => z.UserRole).WithMany().HasForeignKey(z => z.UserRoleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
|
{
|
||||||
|
class UserRoleConfiguration : IEntityTypeConfiguration<UserRole>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserRole> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("UserRole").HasKey(z => z.UserRoleId);
|
||||||
|
builder.Property(z => z.UserRoleId).ValueGeneratedOnAdd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
using Tuitio.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,6 @@ namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
public void Configure(EntityTypeBuilder<UserToken> builder)
|
public void Configure(EntityTypeBuilder<UserToken> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("UserToken").HasKey(z => z.TokenId);
|
builder.ToTable("UserToken").HasKey(z => z.TokenId);
|
||||||
builder.Property(z => z.TokenId).ValueGeneratedOnAdd();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
|
{
|
||||||
|
class UserXUserGroupConfiguration : IEntityTypeConfiguration<UserXUserGroup>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserXUserGroup> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("UserXUserGroup").HasKey(z => new { z.UserId, z.UserGroupId });
|
||||||
|
builder.HasOne(z => z.UserGroup).WithMany().HasForeignKey(z => z.UserGroupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Data.EntityTypeConfiguration
|
||||||
|
{
|
||||||
|
class UserXUserRoleConfiguration : IEntityTypeConfiguration<UserXUserRole>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserXUserRole> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("UserXUserRole").HasKey(z => new { z.UserId, z.UserRoleId });
|
||||||
|
builder.HasOne(z => z.UserRole).WithMany().HasForeignKey(z => z.UserRoleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,56 +0,0 @@
|
||||||
// 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;
|
|
||||||
|
|
||||||
namespace Tuitio.Domain.Data.Repositories
|
|
||||||
{
|
|
||||||
class IdentityRepository : IIdentityRepository
|
|
||||||
{
|
|
||||||
private readonly IdentityDbContext _dbContext;
|
|
||||||
|
|
||||||
public IdentityRepository(IdentityDbContext dbContext)
|
|
||||||
{
|
|
||||||
_dbContext = dbContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<AppUser> GetUser(string userName, string password)
|
|
||||||
{
|
|
||||||
return _dbContext.Users
|
|
||||||
.Include(z => z.Status)
|
|
||||||
.Include(z => z.Claims)
|
|
||||||
.FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUserAfterAuthentication(AppUser user, Token token)
|
|
||||||
{
|
|
||||||
var userToken = new UserToken()
|
|
||||||
{
|
|
||||||
UserId = user.UserId,
|
|
||||||
Token = token.Raw,
|
|
||||||
ValidFrom = token.ValidFrom,
|
|
||||||
ValidUntil = token.ValidUntil
|
|
||||||
};
|
|
||||||
await _dbContext.AddAsync(userToken);
|
|
||||||
|
|
||||||
user.LastLoginDate = DateTime.Now;
|
|
||||||
_dbContext.Update(user);
|
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<UserToken[]> GetActiveTokens()
|
|
||||||
{
|
|
||||||
var currentDate = DateTime.Now;
|
|
||||||
var query = _dbContext.UserTokens
|
|
||||||
.Where(z => z.ValidFrom <= currentDate && z.ValidUntil >= currentDate && (!z.Burnt.HasValue || z.Burnt.Value == false));
|
|
||||||
return query.ToArrayAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
class UserRepository : IUserRepository
|
||||||
|
{
|
||||||
|
private readonly TuitioDbContext _dbContext;
|
||||||
|
|
||||||
|
public UserRepository(TuitioDbContext dbContext)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<AppUser> GetUser(string userName, string password)
|
||||||
|
{
|
||||||
|
return _dbContext.Users
|
||||||
|
.Include(z => z.Status)
|
||||||
|
.Include(z => z.Claims)
|
||||||
|
.Include(z => z.UserRoles).ThenInclude(z => z.UserRole)
|
||||||
|
.Include(z => z.UserGroups).ThenInclude(z => z.UserGroup).ThenInclude(z => z.GroupRoles).ThenInclude(z => z.UserRole)
|
||||||
|
.FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<AppUser> GetFullUser(int userId)
|
||||||
|
{
|
||||||
|
return _dbContext.Users
|
||||||
|
.Include(z => z.Status)
|
||||||
|
.Include(z => z.Claims)
|
||||||
|
.Include(z => z.UserRoles).ThenInclude(z => z.UserRole)
|
||||||
|
.Include(z => z.ContactOptions).ThenInclude(z => z.ContactType)
|
||||||
|
.Include(z => z.UserGroups).ThenInclude(z => z.UserGroup).ThenInclude(z => z.GroupRoles).ThenInclude(z => z.UserRole)
|
||||||
|
.FirstOrDefaultAsync(z => z.UserId == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateUserAfterLogin(AppUser user, Token token, string tokenRaw)
|
||||||
|
{
|
||||||
|
var userToken = new UserToken()
|
||||||
|
{
|
||||||
|
TokenId = token.TokenId,
|
||||||
|
UserId = token.UserId,
|
||||||
|
Token = tokenRaw,
|
||||||
|
ValidFrom = token.CreatedAt,
|
||||||
|
ValidUntil = token.CreatedAt.AddSeconds(token.ExpiresIn)
|
||||||
|
};
|
||||||
|
|
||||||
|
await _dbContext.AddAsync(userToken);
|
||||||
|
user.LastLoginDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
var tokens = await query.ToArrayAsync();
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveToken(Guid tokenId)
|
||||||
|
{
|
||||||
|
var token = new UserToken()
|
||||||
|
{
|
||||||
|
TokenId = tokenId
|
||||||
|
};
|
||||||
|
|
||||||
|
_dbContext.UserTokens.Attach(token);
|
||||||
|
_dbContext.UserTokens.Remove(token);
|
||||||
|
|
||||||
|
return _dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,10 @@ if not exists (select top 1 1 from sys.objects where name = 'UserToken' and type
|
||||||
begin
|
begin
|
||||||
create table UserToken
|
create table UserToken
|
||||||
(
|
(
|
||||||
TokenId int identity(1, 1) constraint PK_Token primary key,
|
TokenId uniqueidentifier constraint PK_Token primary key,
|
||||||
UserId int not null constraint FK_Token_AppUser foreign key references AppUser(UserId),
|
UserId int not null constraint FK_Token_AppUser foreign key references AppUser(UserId),
|
||||||
Token varchar(1000) not null,
|
Token varchar(1000) not null,
|
||||||
ValidFrom datetime not null,
|
ValidFrom datetime not null,
|
||||||
ValidUntil datetime not null,
|
ValidUntil datetime not null
|
||||||
Burnt bit
|
|
||||||
)
|
)
|
||||||
end
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
if not exists (select top 1 1 from sys.objects where name = 'ContactType' and type = 'U')
|
||||||
|
begin
|
||||||
|
create table ContactType
|
||||||
|
(
|
||||||
|
ContactTypeId int identity(1, 1) constraint PK_ContactType primary key,
|
||||||
|
ContactTypeCode varchar(30) not null,
|
||||||
|
ContactTypeName varchar(50) not null
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not exists (select top 1 1 from ContactType)
|
||||||
|
begin
|
||||||
|
insert into ContactType(ContactTypeCode, ContactTypeName)
|
||||||
|
values ('EMAIL', 'Email'),
|
||||||
|
('PHONE', 'Phone'),
|
||||||
|
('WEBSITE', 'Website'),
|
||||||
|
('LINKEDIN', 'LinkedIn'),
|
||||||
|
('GITHUB', 'GitHub'),
|
||||||
|
('GITEA', 'Gitea'),
|
||||||
|
('PORTFOLIO', 'Portfolio'),
|
||||||
|
('CURRICULUM_VITAE', 'Curriculum vitae'),
|
||||||
|
('BLOG', 'Blog'),
|
||||||
|
('REDDIT', 'Reddit')
|
||||||
|
end
|
|
@ -0,0 +1,10 @@
|
||||||
|
if not exists (select top 1 1 from sys.objects where name = 'ContactOption' and type = 'U')
|
||||||
|
begin
|
||||||
|
create table ContactOption
|
||||||
|
(
|
||||||
|
ContactOptionId int identity(1, 1) constraint PK_ContactOption primary key,
|
||||||
|
UserId int constraint FK_ContactOption_AppUser foreign key references AppUser(UserId),
|
||||||
|
ContactTypeId int constraint FK_ContactOption_ContactType foreign key references ContactType(ContactTypeId),
|
||||||
|
ContactValue varchar(150) not null
|
||||||
|
)
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
if not exists (select top 1 1 from sys.objects where name = 'UserGroup' and type = 'U')
|
||||||
|
begin
|
||||||
|
create table UserGroup
|
||||||
|
(
|
||||||
|
UserGroupId int identity(1, 1) constraint PK_UserGroup primary key,
|
||||||
|
UserGroupCode varchar(30) not null,
|
||||||
|
UserGroupName varchar(50) not null
|
||||||
|
)
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
if not exists (select top 1 1 from sys.objects where name = 'UserRole' and type = 'U')
|
||||||
|
begin
|
||||||
|
create table UserRole
|
||||||
|
(
|
||||||
|
UserRoleId int identity(1, 1) constraint PK_UserRole primary key,
|
||||||
|
UserRoleCode varchar(30) not null,
|
||||||
|
UserRoleName varchar(50) not null
|
||||||
|
)
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
if not exists (select top 1 1 from sys.objects where name = 'UserXUserGroup' and type = 'U')
|
||||||
|
begin
|
||||||
|
create table UserXUserGroup
|
||||||
|
(
|
||||||
|
UserId int not null constraint FK_UserXUserGroup_AppUser foreign key references AppUser(UserId),
|
||||||
|
UserGroupId int not null constraint FK_UserXUserGroup_UserGroup foreign key references UserGroup(UserGroupId),
|
||||||
|
constraint PK_UserXUserGroup primary key (UserId, UserGroupId)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not exists (select top 1 1 from sys.objects where name = 'UserGroupXUserRole' and type = 'U')
|
||||||
|
begin
|
||||||
|
create table UserGroupXUserRole
|
||||||
|
(
|
||||||
|
UserGroupId int not null constraint FK_UserGroupXUserRole_UserGroup references UserGroup(UserGroupId),
|
||||||
|
UserRoleId int not null constraint FK_UserGroupXUserRole_UserRole references UserRole(UserRoleId),
|
||||||
|
constraint PK_UserGroupXUserRole primary key (UserGroupId, UserRoleId)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not exists (select top 1 1 from sys.objects where name = 'UserXUserRole' and type = 'U')
|
||||||
|
begin
|
||||||
|
create table UserXUserRole
|
||||||
|
(
|
||||||
|
UserId int not null constraint FK_UserXUserRole_AppUser references AppUser(UserId),
|
||||||
|
UserRoleId int not null constraint FK_UserXUserRole_UserRole references UserRole(UserRoleId),
|
||||||
|
constraint PK_UserXUserRole primary key (UserId, UserRoleId)
|
||||||
|
)
|
||||||
|
end
|
|
@ -28,6 +28,21 @@
|
||||||
<None Update="Scripts\1.0.1\02.UserToken table.sql">
|
<None Update="Scripts\1.0.1\02.UserToken table.sql">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Scripts\2.3.0\01.ContactType table.sql">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Scripts\2.3.0\02.ContactOption table.sql">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Scripts\2.4.0\01.UserGroup table.sql">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Scripts\2.4.0\02.UserRole table.sql">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Scripts\2.4.0\03.New tables UserXUserGroup UserGroupXUserRole UserXUserRole.sql">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -22,5 +22,8 @@ namespace Tuitio.Domain.Entities
|
||||||
public DateTime? PasswordChangeDate { get; set; }
|
public DateTime? PasswordChangeDate { get; set; }
|
||||||
public UserStatus Status { get; set; }
|
public UserStatus Status { get; set; }
|
||||||
public ICollection<UserClaim> Claims { get; set; }
|
public ICollection<UserClaim> Claims { get; set; }
|
||||||
|
public ICollection<ContactOption> ContactOptions { get; set; }
|
||||||
|
public ICollection<UserXUserGroup> UserGroups { get; set; }
|
||||||
|
public ICollection<UserXUserRole> UserRoles { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Entities
|
||||||
|
{
|
||||||
|
public class ContactOption
|
||||||
|
{
|
||||||
|
public int ContactOptionId { get; set; }
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public int ContactTypeId { get; set; }
|
||||||
|
public string ContactValue { get; set; }
|
||||||
|
public ContactType ContactType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Entities
|
||||||
|
{
|
||||||
|
public class ContactType
|
||||||
|
{
|
||||||
|
public int ContactTypeId { get; set; }
|
||||||
|
public string ContactTypeCode { get; set; }
|
||||||
|
public string ContactTypeName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Entities
|
||||||
|
{
|
||||||
|
public class UserGroup
|
||||||
|
{
|
||||||
|
public int UserGroupId { get; set; }
|
||||||
|
public string UserGroupCode { get; set; }
|
||||||
|
public string UserGroupName { get; set; }
|
||||||
|
public ICollection<UserGroupXUserRole> GroupRoles { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Entities
|
||||||
|
{
|
||||||
|
public class UserGroupXUserRole
|
||||||
|
{
|
||||||
|
public int UserGroupId { get; set; }
|
||||||
|
public int UserRoleId { get; set; }
|
||||||
|
public UserRole UserRole { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Entities
|
||||||
|
{
|
||||||
|
public class UserRole
|
||||||
|
{
|
||||||
|
public int UserRoleId { get; set; }
|
||||||
|
public string UserRoleCode { get; set; }
|
||||||
|
public string UserRoleName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,11 +6,10 @@ namespace Tuitio.Domain.Entities
|
||||||
{
|
{
|
||||||
public class UserToken
|
public class UserToken
|
||||||
{
|
{
|
||||||
public int TokenId { get; set; }
|
public Guid TokenId { get; set; }
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
public DateTime ValidFrom { get; set; }
|
public DateTime ValidFrom { get; set; }
|
||||||
public DateTime ValidUntil { get; set; }
|
public DateTime ValidUntil { get; set; }
|
||||||
public bool? Burnt { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Entities
|
||||||
|
{
|
||||||
|
public class UserXUserGroup
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public int UserGroupId { get; set; }
|
||||||
|
public UserGroup UserGroup { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Entities
|
||||||
|
{
|
||||||
|
public class UserXUserRole
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public int UserRoleId { get; set; }
|
||||||
|
public UserRole UserRole { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Helpers
|
||||||
|
{
|
||||||
|
internal static class DataValidationHelper
|
||||||
|
{
|
||||||
|
public static bool StringIsBase64(string str)
|
||||||
|
{
|
||||||
|
str = str.Trim();
|
||||||
|
return (str.Length % 4 == 0) && Regex.IsMatch(str, @"^[a-zA-Z0-9+/]*={0,3}$", RegexOptions.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Models.Account
|
||||||
|
{
|
||||||
|
public record LoginResult(Token Token, string Raw);
|
||||||
|
public record LogoutResult(int UserId, string UserName, DateTime LogoutDate);
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
namespace Tuitio.Domain.Models
|
||||||
|
{
|
||||||
|
public record RecordIdentifier(int Id, string Code);
|
||||||
|
}
|
|
@ -1,13 +1,87 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Tuitio.Domain.Helpers;
|
||||||
|
|
||||||
namespace Tuitio.Domain.Models
|
namespace Tuitio.Domain.Models
|
||||||
{
|
{
|
||||||
public class Token
|
public class Token
|
||||||
{
|
{
|
||||||
public string Raw { get; set; }
|
public Guid TokenId { get; set; }
|
||||||
public DateTime ValidFrom { get; set; }
|
public int UserId { get; set; }
|
||||||
public DateTime ValidUntil { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string FirstName { get; set; }
|
||||||
|
public string LastName { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string SecurityStamp { get; set; }
|
||||||
|
public string LockStamp { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public long ExpiresIn { get; set; }
|
||||||
|
public Dictionary<string, string> Claims { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public IEnumerable<RecordIdentifier> UserRoles { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public IEnumerable<RecordIdentifier> UserGroups { get; set; }
|
||||||
|
|
||||||
|
[Obsolete("This constructor is only used for deserialization and should not be used for any other purpose.")]
|
||||||
|
public Token() { }
|
||||||
|
|
||||||
|
public Token(int validityInMinutes)
|
||||||
|
{
|
||||||
|
TokenId = Guid.NewGuid();
|
||||||
|
CreatedAt = DateTime.UtcNow;
|
||||||
|
LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "");
|
||||||
|
ExpiresIn = validityInMinutes * 60; // seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUserData(int userId, string userName, string firstName, string lastName, string email, string securityStamp, Dictionary<string, string> claims, IEnumerable<RecordIdentifier> userRoles, IEnumerable<RecordIdentifier> userGroups)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
UserName = userName;
|
||||||
|
FirstName = firstName;
|
||||||
|
LastName = lastName;
|
||||||
|
Email = email;
|
||||||
|
SecurityStamp = securityStamp;
|
||||||
|
Claims = claims;
|
||||||
|
UserRoles = userRoles;
|
||||||
|
UserGroups = userGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Export()
|
||||||
|
{
|
||||||
|
var tokenString = JsonConvert.SerializeObject(this);
|
||||||
|
var tokenBytes = Encoding.UTF8.GetBytes(tokenString);
|
||||||
|
var tokenRaw = Convert.ToBase64String(tokenBytes);
|
||||||
|
return tokenRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Token Import(string tokenRaw)
|
||||||
|
{
|
||||||
|
var valid = ValidateTokenRaw(tokenRaw);
|
||||||
|
if (!valid)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var tokenBytes = Convert.FromBase64String(tokenRaw);
|
||||||
|
var tokenString = Encoding.UTF8.GetString(tokenBytes);
|
||||||
|
var token = JsonConvert.DeserializeObject<Token>(tokenString);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ValidateTokenRaw(string tokenRaw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(tokenRaw))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!DataValidationHelper.StringIsBase64(tokenRaw))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Tuitio.Domain.Models
|
|
||||||
{
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,13 +3,16 @@
|
||||||
using Tuitio.Domain.Entities;
|
using Tuitio.Domain.Entities;
|
||||||
using Tuitio.Domain.Models;
|
using Tuitio.Domain.Models;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Tuitio.Domain.Repositories
|
namespace Tuitio.Domain.Repositories
|
||||||
{
|
{
|
||||||
public interface IIdentityRepository
|
public interface IUserRepository
|
||||||
{
|
{
|
||||||
Task<AppUser> GetUser(string userName, string password);
|
Task<AppUser> GetUser(string userName, string password);
|
||||||
Task UpdateUserAfterAuthentication(AppUser user, Token token);
|
Task<AppUser> GetFullUser(int userId);
|
||||||
|
Task UpdateUserAfterLogin(AppUser user, Token token, string tokenRaw);
|
||||||
Task<UserToken[]> GetActiveTokens();
|
Task<UserToken[]> GetActiveTokens();
|
||||||
|
Task RemoveToken(Guid tokenId);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
namespace Tuitio.PublishedLanguage.Constants
|
|
||||||
{
|
|
||||||
public struct AuthenticationStatus
|
|
||||||
{
|
|
||||||
public const string
|
|
||||||
SUCCESS = "SUCCESS",
|
|
||||||
BAD_CREDENTIALS = "BAD_CREDENTIALS";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
namespace Tuitio.PublishedLanguage.Constants
|
||||||
|
{
|
||||||
|
public struct EnvelopeError
|
||||||
|
{
|
||||||
|
public const string
|
||||||
|
BAD_CREDENTIALS = "BAD_CREDENTIALS",
|
||||||
|
UNAUTHENTICATED = "UNAUTHENTICATED",
|
||||||
|
UNAUTHORIZED = "UNAUTHORIZED";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
namespace Tuitio.PublishedLanguage.Dto
|
||||||
|
{
|
||||||
|
public class Envelope<T> where T : class
|
||||||
|
{
|
||||||
|
public T Result { get; }
|
||||||
|
public string Error { get; }
|
||||||
|
|
||||||
|
public Envelope(T result, string error)
|
||||||
|
{
|
||||||
|
Result=result;
|
||||||
|
Error=error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Envelope<T> Success(T result)
|
||||||
|
{
|
||||||
|
return new Envelope<T>(result, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Envelope<T> Fail(string message)
|
||||||
|
{
|
||||||
|
return new Envelope<T>(null, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Tuitio.PublishedLanguage.Dto
|
||||||
|
{
|
||||||
|
public record AccountLoginResult(string Token, double ExpiresIn);
|
||||||
|
public record AccountLogoutResult(int UserId, string UserName, DateTime LogoutDate);
|
||||||
|
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 SecurityStamp { get; init; }
|
||||||
|
public string LockStamp { get; init; }
|
||||||
|
public DateTime CreatedAt { get; init; }
|
||||||
|
public long ExpiresIn { get; init; }
|
||||||
|
public Dictionary<string, string> Claims { get; init; }
|
||||||
|
public RecordIdentifier[] UserRoles { get; init; }
|
||||||
|
public RecordIdentifier[] UserGroups { get; init; }
|
||||||
|
}
|
||||||
|
public record RecordIdentifier(int Id, string Code);
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Tuitio.PublishedLanguage.Dto
|
|
||||||
{
|
|
||||||
public class Token
|
|
||||||
{
|
|
||||||
public string Raw { get; set; }
|
|
||||||
public DateTime ValidFrom { get; set; }
|
|
||||||
public DateTime ValidUntil { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using Tuitio.PublishedLanguage.Dto;
|
|
||||||
|
|
||||||
namespace Tuitio.PublishedLanguage.Events
|
|
||||||
{
|
|
||||||
public class AuthenticateUserResult
|
|
||||||
{
|
|
||||||
public Token Token { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,16 @@
|
||||||
2.0.0 release [2023-01-31 02:17]
|
2.2.2 release [2023-04-12 20:37]
|
||||||
|
◾ Added IDs for user roles and groups in authorization result
|
||||||
|
|
||||||
|
2.2.1 release [2023-04-08 01:48]
|
||||||
|
◾ Added user roles and groups in authorization result
|
||||||
|
|
||||||
|
2.2.0 release [2023-03-27 19:20]
|
||||||
|
◾ Added "user-info" method in API
|
||||||
|
|
||||||
|
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
|
◾ Tuitio rebranding
|
||||||
◾ Initial release of Tuitio's published language package
|
◾ Initial release of Tuitio's published language package
|
|
@ -7,7 +7,7 @@
|
||||||
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
|
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
|
||||||
<RepositoryType>Git</RepositoryType>
|
<RepositoryType>Git</RepositoryType>
|
||||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||||
<Version>2.0.0</Version>
|
<Version>2.2.2</Version>
|
||||||
<PackageIcon>logo.png</PackageIcon>
|
<PackageIcon>logo.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<Company>Toodle HomeLab</Company>
|
<Company>Toodle HomeLab</Company>
|
||||||
|
|
|
@ -5,7 +5,8 @@ namespace Tuitio.Wrapper.Constants
|
||||||
internal struct ApiRoutes
|
internal struct ApiRoutes
|
||||||
{
|
{
|
||||||
public const string
|
public const string
|
||||||
Authentication = "identity/authenticate?UserName={0}&Password={1}",
|
AccountLogin = "account/login?UserName={0}&Password={1}",
|
||||||
Authorization = "identity/authorize?Token={0}";
|
AccountLogout = "account/logout?Token={0}",
|
||||||
|
Authorization = "connect/authorize?Token={0}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ namespace Tuitio.Wrapper
|
||||||
public static void UseTuitioServices(this IServiceCollection services, string baseAddress)
|
public static void UseTuitioServices(this IServiceCollection services, string baseAddress)
|
||||||
{
|
{
|
||||||
services.AddSingleton(new ServiceConfiguration(baseAddress));
|
services.AddSingleton(new ServiceConfiguration(baseAddress));
|
||||||
services.AddHttpClient<IIdentityService, IdentityService>();
|
services.AddHttpClient<ITuitioService, TuitioService>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Tuitio.Wrapper.Tests")]
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
[Tuitio](https://lab.code-rove.com/gitea/tudor.stanciu/tuitio) is a simple identity server implementation focused strictly on the needs of my home lab.
|
[Tuitio](https://lab.code-rove.com/gitea/tudor.stanciu/tuitio) is a simple identity server implementation focused strictly on the needs of my home lab.
|
||||||
|
|
||||||
***Tuitio.Wrapper*** is a NuGet package that facilitates integration with a Tuitio instance in a .NET environment by registering a service called IIdentityService in the application's service collection.
|
***Tuitio.Wrapper*** is a NuGet package that facilitates integration with a Tuitio instance in a .NET environment by registering a service called ITuitioService in the application's service collection.
|
||||||
It contains two methods, "Authenticate" and "Authorize", which are responsible for calling the appropriate methods from the API controller, ```/identity/authenticate``` or ```/identity/authorize```. These methods provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio's API.
|
It contains three methods, "Login", "Logout" and "Authorize", which are responsible for calling the appropriate methods from the API controller, ```/account/login```, ```/account/logout``` or ```/connect/authorize```. These methods provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio's API.
|
||||||
Once the package is installed, all the developer has to do is call the ```UseTuitioServices``` method at application startup. After this step, IIdentityService can be injected into any class in the application.
|
Once the package is installed, all the developer has to do is call the ```UseTuitioServices``` method at application startup. After this step, ITuitioService can be injected into any class in the application.
|
||||||
|
|
||||||
|
|
||||||
## Package repository
|
## Package repository
|
||||||
|
|
|
@ -1,3 +1,16 @@
|
||||||
2.0.0 release [2023-01-31 02:17]
|
2.2.2 release [2023-04-12 20:37]
|
||||||
|
◾ Added IDs for user roles and groups in authorization result
|
||||||
|
|
||||||
|
2.2.1 release [2023-04-08 01:48]
|
||||||
|
◾ Added user roles and groups in authorization result
|
||||||
|
|
||||||
|
2.2.0 release [2023-03-27 19:20]
|
||||||
|
◾ Added "user-info" method in API
|
||||||
|
|
||||||
|
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
|
◾ Tuitio rebranding
|
||||||
◾ Initial release of Tuitio's API wrapper
|
◾ Initial release of Tuitio's API wrapper
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using Tuitio.PublishedLanguage.Dto;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tuitio.Wrapper.Services
|
|
||||||
{
|
|
||||||
public interface IIdentityService
|
|
||||||
{
|
|
||||||
Task<Token> Authenticate(string userName, string password);
|
|
||||||
Task<TokenCore> Authorize(string token);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tuitio.PublishedLanguage.Dto;
|
||||||
|
|
||||||
|
namespace Tuitio.Wrapper.Services
|
||||||
|
{
|
||||||
|
public interface ITuitioService
|
||||||
|
{
|
||||||
|
Task<Envelope<AccountLoginResult>> Login(string userName, string password);
|
||||||
|
Task<Envelope<AccountLogoutResult>> Logout(string token);
|
||||||
|
Task<Envelope<AuthorizationResult>> Authorize(string token);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
// 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;
|
|
||||||
|
|
||||||
namespace Tuitio.Wrapper.Services
|
|
||||||
{
|
|
||||||
internal class IdentityService : IIdentityService
|
|
||||||
{
|
|
||||||
private const string _contentType = "application/json";
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
|
|
||||||
public IdentityService(HttpClient httpClient, ServiceConfiguration configuration)
|
|
||||||
{
|
|
||||||
httpClient.BaseAddress = new Uri(configuration.BaseAddress);
|
|
||||||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(_contentType));
|
|
||||||
|
|
||||||
_httpClient = httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Token> Authenticate(string userName, string password)
|
|
||||||
{
|
|
||||||
var route = string.Format(ApiRoutes.Authentication, userName, password);
|
|
||||||
var result = await _httpClient.ExecutePostRequest<Token>(route);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TokenCore> Authorize(string token)
|
|
||||||
{
|
|
||||||
var route = string.Format(ApiRoutes.Authorization, token);
|
|
||||||
var result = await _httpClient.ExecutePostRequest<TokenCore>(route);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
internal class TuitioService : ITuitioService
|
||||||
|
{
|
||||||
|
private const string _contentType = "application/json";
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public TuitioService(HttpClient httpClient, ServiceConfiguration configuration)
|
||||||
|
{
|
||||||
|
httpClient.BaseAddress = new Uri(configuration.BaseAddress);
|
||||||
|
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(_contentType));
|
||||||
|
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Envelope<AccountLoginResult>> Login(string userName, string password)
|
||||||
|
{
|
||||||
|
var route = string.Format(ApiRoutes.AccountLogin, userName, password);
|
||||||
|
var result = await _httpClient.ExecutePostRequest<Envelope<AccountLoginResult>>(route);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Envelope<AccountLogoutResult>> Logout(string token)
|
||||||
|
{
|
||||||
|
var route = string.Format(ApiRoutes.AccountLogout, token);
|
||||||
|
var result = await _httpClient.ExecutePostRequest<Envelope<AccountLogoutResult>>(route);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Envelope<AuthorizationResult>> Authorize(string token)
|
||||||
|
{
|
||||||
|
var route = string.Format(ApiRoutes.Authorization, token);
|
||||||
|
var result = await _httpClient.ExecutePostRequest<Envelope<AuthorizationResult>>(route);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Description>Tuitio.Wrapper facilitates integration with a Tuitio instance in a .NET environment by registering a service called IIdentityService in the application’s service collection. It contains two methods that provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio’s API.</Description>
|
<Description>Tuitio.Wrapper facilitates integration with a Tuitio instance in a .NET environment by registering a service called ITuitioService in the application’s service collection. It contains three methods that provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio’s API.</Description>
|
||||||
<PackageProjectUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</PackageProjectUrl>
|
<PackageProjectUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</PackageProjectUrl>
|
||||||
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
|
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
|
||||||
<RepositoryType>Git</RepositoryType>
|
<RepositoryType>Git</RepositoryType>
|
||||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||||
<Version>2.0.0</Version>
|
<Version>2.2.2</Version>
|
||||||
<PackageIcon>logo.png</PackageIcon>
|
<PackageIcon>logo.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<Company>Toodle HomeLab</Company>
|
<Company>Toodle HomeLab</Company>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Tuitio.Application.Abstractions;
|
||||||
|
|
||||||
|
namespace Tuitio.Authentication
|
||||||
|
{
|
||||||
|
internal static class AuthenticationExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddLocalAuthentication(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
var scheme = "TuitioAuthentication";
|
||||||
|
services.AddAuthentication(scheme)
|
||||||
|
.AddScheme<AuthenticationSchemeOptions, AuthenticationHandler>(scheme, null);
|
||||||
|
services.AddScoped<IHttpContextService, HttpContextService>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tuitio.Application.Services.Abstractions;
|
||||||
|
using Tuitio.Domain.Models;
|
||||||
|
|
||||||
|
namespace Tuitio.Authentication
|
||||||
|
{
|
||||||
|
public class AuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly ILogger<AuthenticationHandler> _logger;
|
||||||
|
|
||||||
|
public AuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory loggerFactory, UrlEncoder encoder, ISystemClock clock, IUserService userService, ILogger<AuthenticationHandler> logger) : base(options, loggerFactory, encoder, clock)
|
||||||
|
{
|
||||||
|
_userService=userService;
|
||||||
|
_logger=logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
var endpoint = Context.GetEndpoint();
|
||||||
|
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
|
||||||
|
{
|
||||||
|
return AuthenticateResult.NoResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = GetAuthorizationToken();
|
||||||
|
if (token == null)
|
||||||
|
return AuthenticateResult.Fail("AUTHORIZATION_HEADER_IS_MISSING");
|
||||||
|
|
||||||
|
var result = await Task.Run(() => _userService.Authorize(token));
|
||||||
|
if (result == null)
|
||||||
|
return AuthenticateResult.Fail("UNAUTHORIZED");
|
||||||
|
|
||||||
|
var ticket = GetAuthenticationTicket(result);
|
||||||
|
return AuthenticateResult.Success(ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetAuthorizationToken()
|
||||||
|
{
|
||||||
|
if (Request.Headers.ContainsKey("Authorization"))
|
||||||
|
{
|
||||||
|
var authorizationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
|
||||||
|
var token = authorizationHeader.Parameter;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationTicket GetAuthenticationTicket(Token result)
|
||||||
|
{
|
||||||
|
var claimCollection = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ ClaimTypes.NameIdentifier, result.UserId.ToString() },
|
||||||
|
{ ClaimTypes.Name, result.UserName },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.FirstName != null)
|
||||||
|
claimCollection.Add(ClaimTypes.GivenName, result.FirstName);
|
||||||
|
|
||||||
|
if (result.LastName != null)
|
||||||
|
claimCollection.Add(ClaimTypes.Surname, result.FirstName);
|
||||||
|
|
||||||
|
if (result.Email != null)
|
||||||
|
claimCollection.Add(ClaimTypes.Email, result.Email);
|
||||||
|
|
||||||
|
if (result.Claims != null && result.Claims.Any())
|
||||||
|
{
|
||||||
|
foreach (var claim in result.Claims)
|
||||||
|
{
|
||||||
|
if (claimCollection.ContainsKey(claim.Key))
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"There is already a claim with key {claim.Key} in the collection. The combination {claim.Key}:{claim.Value} will be ignored.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
claimCollection.Add(claim.Key, claim.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims = claimCollection.Select(z => new Claim(z.Key, z.Value)).ToArray();
|
||||||
|
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||||
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||||
|
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System.Linq;
|
||||||
|
using System;
|
||||||
|
using Tuitio.Application.Abstractions;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace Tuitio.Authentication
|
||||||
|
{
|
||||||
|
public class HttpContextService : IHttpContextService
|
||||||
|
{
|
||||||
|
private readonly IHttpContextAccessor _httpAccessor;
|
||||||
|
|
||||||
|
public HttpContextService(IHttpContextAccessor httpAccessor)
|
||||||
|
{
|
||||||
|
_httpAccessor=httpAccessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetUserId()
|
||||||
|
{
|
||||||
|
var userIdString = _httpAccessor.HttpContext.User?.Claims.FirstOrDefault(z => z.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userIdString))
|
||||||
|
throw new Exception("User id could not be retrieved from claims.");
|
||||||
|
|
||||||
|
if (!int.TryParse(userIdString, out int userId))
|
||||||
|
throw new Exception("User id is not a valid integer.");
|
||||||
|
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tuitio.Application.CommandHandlers;
|
||||||
|
|
||||||
|
namespace Tuitio.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
[Route("account")]
|
||||||
|
public class AccountController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public AccountController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<IActionResult> Login([FromQuery] string userName, string password)
|
||||||
|
{
|
||||||
|
var command = new AccountLoginHandler.Command(userName, password);
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("logout")]
|
||||||
|
public async Task<IActionResult> Logout([FromQuery] string token)
|
||||||
|
{
|
||||||
|
var command = new AccountLogoutHandler.Command(token);
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tuitio.Application.CommandHandlers;
|
||||||
|
using Tuitio.Application.Queries;
|
||||||
|
|
||||||
|
namespace Tuitio.Api.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
[Route("connect")]
|
||||||
|
public class ConnectController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public ConnectController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("authorize")]
|
||||||
|
public async Task<IActionResult> AuthorizeToken([FromQuery] string token)
|
||||||
|
{
|
||||||
|
var command = new AuthorizationHandler.Command(token);
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("user-info")]
|
||||||
|
public async Task<IActionResult> GetUserInfo()
|
||||||
|
{
|
||||||
|
var command = new GetUserInfo.Query();
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,43 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using Tuitio.Application.Commands;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tuitio.Api.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("identity")]
|
|
||||||
public class IdentityController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
|
|
||||||
public IdentityController(IMediator mediator)
|
|
||||||
{
|
|
||||||
_mediator = mediator;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("authenticate")]
|
|
||||||
public async Task<IActionResult> AuthenticateUser([FromQuery] AuthenticateUser authenticateUser)
|
|
||||||
{
|
|
||||||
var result = await _mediator.Send(authenticateUser);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
return Ok(result);
|
|
||||||
else
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("authorize")]
|
|
||||||
public async Task<IActionResult> AuthorizeToken([FromQuery] AuthorizeToken authorizeToken)
|
|
||||||
{
|
|
||||||
var result = await _mediator.Send(authorizeToken);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
return Ok(result);
|
|
||||||
else
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,11 +11,8 @@ namespace Tuitio.Api.Controllers
|
||||||
[Route("system")]
|
[Route("system")]
|
||||||
public class SystemController : ControllerBase
|
public class SystemController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
|
||||||
|
|
||||||
public SystemController(IMediator mediator)
|
public SystemController(IMediator mediator)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
@ -24,12 +21,5 @@ namespace Tuitio.Api.Controllers
|
||||||
{
|
{
|
||||||
return Ok($"Ping success. System datetime: {DateTime.Now}");
|
return Ok($"Ping success. System datetime: {DateTime.Now}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Methods:
|
|
||||||
/version
|
|
||||||
/burn-token
|
|
||||||
/burn-all-tokens
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,9 @@ COPY ["NuGet.config", "."]
|
||||||
COPY ["src/Tuitio/Tuitio.csproj", "src/Tuitio/"]
|
COPY ["src/Tuitio/Tuitio.csproj", "src/Tuitio/"]
|
||||||
COPY ["src/Tuitio.Application/Tuitio.Application.csproj", "src/Tuitio.Application/"]
|
COPY ["src/Tuitio.Application/Tuitio.Application.csproj", "src/Tuitio.Application/"]
|
||||||
COPY ["src/Tuitio.Domain/Tuitio.Domain.csproj", "src/Tuitio.Domain/"]
|
COPY ["src/Tuitio.Domain/Tuitio.Domain.csproj", "src/Tuitio.Domain/"]
|
||||||
COPY ["src/Tuitio.PublishedLanguage/Tuitio.PublishedLanguage.csproj", "src/Tuitio.PublishedLanguage/"]
|
|
||||||
COPY ["src/Tuitio.Domain.Data/Tuitio.Domain.Data.csproj", "src/Tuitio.Domain.Data/"]
|
COPY ["src/Tuitio.Domain.Data/Tuitio.Domain.Data.csproj", "src/Tuitio.Domain.Data/"]
|
||||||
|
COPY ["src/Tuitio.PublishedLanguage/Tuitio.PublishedLanguage.csproj", "src/Tuitio.PublishedLanguage/"]
|
||||||
|
COPY ["src/Tuitio.PublishedLanguage/ReleaseNotes.txt", "src/Tuitio.PublishedLanguage/"]
|
||||||
RUN dotnet restore "src/Tuitio/Tuitio.csproj"
|
RUN dotnet restore "src/Tuitio/Tuitio.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR "/workspace/src/Tuitio"
|
WORKDIR "/workspace/src/Tuitio"
|
||||||
|
|
|
@ -11,6 +11,7 @@ using Netmash.Infrastructure.DatabaseMigration;
|
||||||
using Netmash.Infrastructure.DatabaseMigration.Constants;
|
using Netmash.Infrastructure.DatabaseMigration.Constants;
|
||||||
using Tuitio.Application;
|
using Tuitio.Application;
|
||||||
using Tuitio.Application.Services.Abstractions;
|
using Tuitio.Application.Services.Abstractions;
|
||||||
|
using Tuitio.Authentication;
|
||||||
using Tuitio.Domain.Data;
|
using Tuitio.Domain.Data;
|
||||||
|
|
||||||
namespace Tuitio.Extensions
|
namespace Tuitio.Extensions
|
||||||
|
@ -20,18 +21,19 @@ namespace Tuitio.Extensions
|
||||||
public static void ConfigureServices(this IServiceCollection services, IConfiguration configuration)
|
public static void ConfigureServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddControllers();
|
services.AddControllers();
|
||||||
|
services.AddLocalAuthentication();
|
||||||
|
services.AddHttpContextAccessor();
|
||||||
|
|
||||||
// MediatR
|
// MediatR
|
||||||
services.AddMediatR(typeof(Application.Commands.AuthenticateUser).Assembly);
|
services.AddMediatR(typeof(Application.CommandHandlers.AccountLoginHandler).Assembly);
|
||||||
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>));
|
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>));
|
||||||
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>));
|
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>));
|
||||||
|
|
||||||
// AutoMapper
|
// AutoMapper
|
||||||
services.AddAutoMapper(
|
services.AddAutoMapper(typeof(Application.Mappings.MappingProfile).Assembly);
|
||||||
typeof(Application.Mappings.MappingProfile).Assembly);
|
|
||||||
|
|
||||||
// Swagger
|
// Swagger
|
||||||
services.AddSwagger("Tuitio API", AuthorizationType.None);
|
services.AddSwagger("Tuitio API", AuthorizationType.Tuitio);
|
||||||
|
|
||||||
// Data access
|
// Data access
|
||||||
services.AddMigration(DatabaseType.SQLServer, MetadataLocation.Database);
|
services.AddMigration(DatabaseType.SQLServer, MetadataLocation.Database);
|
||||||
|
@ -48,6 +50,7 @@ namespace Tuitio.Extensions
|
||||||
app.UseCors(z => z.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
|
app.UseCors(z => z.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,10 +5,7 @@
|
||||||
},
|
},
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information"
|
||||||
"Override": {
|
|
||||||
"Microsoft": "Information"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logs": {
|
"Logs": {
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tuitio.Application.CommandHandlers;
|
||||||
|
using Tuitio.Application.Tests.Fixtures;
|
||||||
|
using Tuitio.PublishedLanguage.Constants;
|
||||||
|
using Tuitio.PublishedLanguage.Dto;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Tests
|
||||||
|
{
|
||||||
|
public class CommandHandlerTests : IClassFixture<DependencyInjectionFixture>, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IServiceScope _tuitioScope;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public CommandHandlerTests(DependencyInjectionFixture fixture)
|
||||||
|
{
|
||||||
|
_tuitioScope = fixture.ServiceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||||
|
_mediator = _tuitioScope.ServiceProvider.GetRequiredService<IMediator>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_tuitioScope.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AccountLoginHandler_Handle_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userName = "tuitio.user";
|
||||||
|
var password = "pass123";
|
||||||
|
var command = new AccountLoginHandler.Command(userName, password);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var loginResult = await _mediator.Send(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(loginResult);
|
||||||
|
Assert.NotNull(loginResult.Result);
|
||||||
|
Assert.Null(loginResult.Error);
|
||||||
|
Assert.NotEmpty(loginResult.Result.Token);
|
||||||
|
Assert.True(loginResult.Result.ExpiresIn > 0, "Token expiration must be a positive number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AccountLoginHandler_Handle_Failed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userName = "tuitio.user";
|
||||||
|
var password = "wrong_password";
|
||||||
|
var command = new AccountLoginHandler.Command(userName, password);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Null(result.Result);
|
||||||
|
Assert.NotNull(result.Error);
|
||||||
|
Assert.NotEmpty(result.Error);
|
||||||
|
Assert.Equal(EnvelopeError.BAD_CREDENTIALS, result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AccountLogoutHandler_Handle_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userName = "tuitio.user";
|
||||||
|
var password = "pass123";
|
||||||
|
var loginCommand = new AccountLoginHandler.Command(userName, password);
|
||||||
|
var loginResult = await _mediator.Send(loginCommand);
|
||||||
|
var logoutCommand = new AccountLogoutHandler.Command(loginResult.Result.Token);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Envelope<AccountLogoutResult> logoutResult;
|
||||||
|
using (var scope = _tuitioScope.ServiceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
var newMediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||||
|
logoutResult = await newMediator.Send(logoutCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(logoutResult);
|
||||||
|
Assert.NotNull(logoutResult.Result);
|
||||||
|
Assert.Null(logoutResult.Error);
|
||||||
|
Assert.Equal(userName, logoutResult.Result.UserName);
|
||||||
|
Assert.True((DateTime.UtcNow - logoutResult.Result.LogoutDate).TotalMinutes <= 1, "Logout date must be within the last minute.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AccountLogoutHandler_Handle_Failed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unauthenticatedToken = "unauthenticated-token";
|
||||||
|
var command = new AccountLogoutHandler.Command(unauthenticatedToken);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Null(result.Result);
|
||||||
|
Assert.NotNull(result.Error);
|
||||||
|
Assert.NotEmpty(result.Error);
|
||||||
|
Assert.Equal(EnvelopeError.UNAUTHENTICATED, result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthorizationHandler_Handle_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userName = "tuitio.user";
|
||||||
|
var password = "pass123";
|
||||||
|
var loginCommand = new AccountLoginHandler.Command(userName, password);
|
||||||
|
var loginResult = await _mediator.Send(loginCommand);
|
||||||
|
var authorizationCommand = new AuthorizationHandler.Command(loginResult.Result.Token);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var authorizationResult = await _mediator.Send(authorizationCommand);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(authorizationResult);
|
||||||
|
Assert.NotNull(authorizationResult.Result);
|
||||||
|
Assert.Null(authorizationResult.Error);
|
||||||
|
Assert.Equal(userName, authorizationResult.Result.UserName);
|
||||||
|
Assert.NotNull(authorizationResult.Result.SecurityStamp);
|
||||||
|
Assert.NotNull(authorizationResult.Result.LockStamp);
|
||||||
|
Assert.True(authorizationResult.Result.TokenId != Guid.Empty, "Token id cannot be an empty guid.");
|
||||||
|
Assert.True(authorizationResult.Result.ExpiresIn > 0, "Token expiration must be a positive number.");
|
||||||
|
Assert.True(authorizationResult.Result.UserRoles.Any(), "User must have at least one role.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthorizationHandler_Handle_Failed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unauthorizedToken = "unauthorized-token";
|
||||||
|
var command = new AuthorizationHandler.Command(unauthorizedToken);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _mediator.Send(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Null(result.Result);
|
||||||
|
Assert.NotNull(result.Error);
|
||||||
|
Assert.NotEmpty(result.Error);
|
||||||
|
Assert.Equal(EnvelopeError.UNAUTHORIZED, result.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using MediatR;
|
||||||
|
using MediatR.Pipeline;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tuitio.Domain.Data;
|
||||||
|
using Tuitio.Domain.Data.DbContexts;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Tests.Fixtures
|
||||||
|
{
|
||||||
|
public class DependencyInjectionFixture : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private readonly Guid _prefix;
|
||||||
|
private readonly List<SqliteConnection> _connections;
|
||||||
|
public readonly IServiceProvider ServiceProvider;
|
||||||
|
|
||||||
|
public DependencyInjectionFixture()
|
||||||
|
{
|
||||||
|
_prefix = Guid.NewGuid();
|
||||||
|
_connections = new List<SqliteConnection>();
|
||||||
|
ServiceProvider = BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
foreach (var con in _connections)
|
||||||
|
await con.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
await PrepareDbAsync();
|
||||||
|
SeedData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IServiceProvider BuildServiceProvider()
|
||||||
|
{
|
||||||
|
var configurationBuilder = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddJsonFile("appsettings.tuitio.json", optional: true, reloadOnChange: false);
|
||||||
|
var configuration = configurationBuilder.Build();
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<IConfiguration>(configuration);
|
||||||
|
services.AddLogging(builder =>
|
||||||
|
{
|
||||||
|
builder.SetMinimumLevel(LogLevel.Warning);
|
||||||
|
builder.AddFilter("Microsoft.*", LogLevel.Warning);
|
||||||
|
});
|
||||||
|
|
||||||
|
// MediatR
|
||||||
|
services.AddMediatR(typeof(Application.CommandHandlers.AccountLoginHandler).Assembly);
|
||||||
|
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>));
|
||||||
|
|
||||||
|
// AutoMapper
|
||||||
|
services.AddAutoMapper(typeof(Application.Mappings.MappingProfile).Assembly);
|
||||||
|
|
||||||
|
services.AddDbContext<TuitioDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.UseSqlite(GetOrAddInMemoryDatabase("TuitioDbContext"));
|
||||||
|
options.EnableSensitiveDataLogging();
|
||||||
|
options.EnableDetailedErrors();
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddDataAccessServices();
|
||||||
|
services.AddApplicationServices();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DbConnection GetOrAddInMemoryDatabase(string code)
|
||||||
|
{
|
||||||
|
var connection = new SqliteConnection($"Data Source={_prefix}{code};Mode=Memory;Cache=shared");
|
||||||
|
_connections.Add(connection);
|
||||||
|
connection.Open();
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PrepareDbAsync()
|
||||||
|
{
|
||||||
|
var result = await ServiceProvider.GetRequiredService<TuitioDbContext>().Database.EnsureCreatedAsync();
|
||||||
|
if (!result) throw new Exception("TuitioDbContext exists");
|
||||||
|
|
||||||
|
var script = await File.ReadAllTextAsync("seed.sql");
|
||||||
|
await ServiceProvider.GetRequiredService<TuitioDbContext>().Database.ExecuteSqlRawAsync(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SeedData()
|
||||||
|
{
|
||||||
|
using var dbContext = ServiceProvider.GetRequiredService<TuitioDbContext>();
|
||||||
|
|
||||||
|
#region UserStatus
|
||||||
|
dbContext.UserStatuses.AddRange(new Domain.Entities.UserStatus[]
|
||||||
|
{
|
||||||
|
new Domain.Entities.UserStatus()
|
||||||
|
{
|
||||||
|
StatusId = 1,
|
||||||
|
StatusCode = "ACTIVE",
|
||||||
|
StatusName = "Active"
|
||||||
|
},
|
||||||
|
new Domain.Entities.UserStatus()
|
||||||
|
{
|
||||||
|
StatusId = 2,
|
||||||
|
StatusCode = "INACTIVE",
|
||||||
|
StatusName = "Inactive"
|
||||||
|
},
|
||||||
|
new Domain.Entities.UserStatus()
|
||||||
|
{
|
||||||
|
StatusId = 3,
|
||||||
|
StatusCode = "BLOCKED",
|
||||||
|
StatusName = "Blocked"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Users
|
||||||
|
dbContext.Users.Add(new Domain.Entities.AppUser()
|
||||||
|
{
|
||||||
|
UserId = 1,
|
||||||
|
UserName = "tuitio.user",
|
||||||
|
Password = "9B8769A4A742959A2D0298C36FB70623F2DFACDA8436237DF08D8DFD5B37374C", // pass123
|
||||||
|
FirstName = "Tuitio",
|
||||||
|
LastName = "User",
|
||||||
|
Email = "tuitio.user@tuitio.lab",
|
||||||
|
SecurityStamp = "A93650FF-1FC4-4999-BAB6-3EEB174F6892",
|
||||||
|
StatusId = 1,
|
||||||
|
CreationDate = DateTime.Now,
|
||||||
|
FailedLoginAttempts = 0,
|
||||||
|
UserRoles = new UserXUserRole[]
|
||||||
|
{
|
||||||
|
new UserXUserRole()
|
||||||
|
{
|
||||||
|
UserId = 1,
|
||||||
|
UserRoleId = 1,
|
||||||
|
UserRole = new UserRole()
|
||||||
|
{
|
||||||
|
UserRoleId = 1,
|
||||||
|
UserRoleCode = "MOCK_ROLE",
|
||||||
|
UserRoleName = "Mock role"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Tuitio.Application.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Tests
|
||||||
|
{
|
||||||
|
public class HashingServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void HashSha256_ShouldReturnCorrectHash()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "9B8769A4A742959A2D0298C36FB70623F2DFACDA8436237DF08D8DFD5B37374C";
|
||||||
|
var hashingService = new HashingService();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = hashingService.HashSha256("pass123");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tuitio.Application.Tests.Fixtures;
|
||||||
|
using Tuitio.Domain.Data.DbContexts;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Tests
|
||||||
|
{
|
||||||
|
public class LocalSqliteDatabaseTests : IClassFixture<DependencyInjectionFixture>, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IServiceScope _tuitioScope;
|
||||||
|
|
||||||
|
public LocalSqliteDatabaseTests(DependencyInjectionFixture fixture)
|
||||||
|
{
|
||||||
|
_tuitioScope = fixture.ServiceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_tuitioScope.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EnsureExpectedDataInLocalDbTest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var _dbContext = _tuitioScope.ServiceProvider.GetRequiredService<TuitioDbContext>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var statuses = await _dbContext.UserStatuses.ToArrayAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotEmpty(statuses);
|
||||||
|
Assert.True(statuses.All(z => !string.IsNullOrWhiteSpace(z.StatusCode)));
|
||||||
|
Assert.Contains(statuses, z => z.StatusCode == "ACTIVE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using Tuitio.Application.Services.Abstractions;
|
||||||
|
using Tuitio.Application.Tests.Fixtures;
|
||||||
|
using Tuitio.Domain.Entities;
|
||||||
|
using Tuitio.Domain.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Tests
|
||||||
|
{
|
||||||
|
public class TokenServiceTests : IClassFixture<DependencyInjectionFixture>, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IServiceScope _tuitioScope;
|
||||||
|
private readonly AppUser _userMock;
|
||||||
|
|
||||||
|
public TokenServiceTests(DependencyInjectionFixture fixture)
|
||||||
|
{
|
||||||
|
_tuitioScope = fixture.ServiceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||||
|
_userMock = MockAppUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppUser MockAppUser()
|
||||||
|
{
|
||||||
|
var user = new AppUser()
|
||||||
|
{
|
||||||
|
UserId = 1,
|
||||||
|
UserName = "tuitio.test",
|
||||||
|
Password = "9B8769A4A742959A2D0298C36FB70623F2DFACDA8436237DF08D8DFD5B37374C", //pass123
|
||||||
|
Email = "tuitio.test@test.test",
|
||||||
|
FirstName = "tuitio",
|
||||||
|
LastName = "test",
|
||||||
|
StatusId = 1,
|
||||||
|
FailedLoginAttempts = 0,
|
||||||
|
SecurityStamp = "A93650FF-1FC4-4999-BAB6-3EEB174F6892",
|
||||||
|
CreationDate = DateTime.Now,
|
||||||
|
UserRoles = new UserXUserRole[]
|
||||||
|
{
|
||||||
|
new UserXUserRole()
|
||||||
|
{
|
||||||
|
UserId = 1,
|
||||||
|
UserRoleId = 1,
|
||||||
|
UserRole = new UserRole()
|
||||||
|
{
|
||||||
|
UserRoleId = 1,
|
||||||
|
UserRoleCode = "MOCK_ROLE",
|
||||||
|
UserRoleName = "Mock role"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_tuitioScope.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TokenGenerationTest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var _tokenService = _tuitioScope.ServiceProvider.GetRequiredService<ITokenService>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var token = _tokenService.GenerateToken(_userMock);
|
||||||
|
var raw = token.Export();
|
||||||
|
var extracted = Token.Import(raw);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(token);
|
||||||
|
Assert.NotNull(raw);
|
||||||
|
Assert.NotNull(extracted);
|
||||||
|
|
||||||
|
Assert.Equal(_userMock.UserName, extracted.UserName);
|
||||||
|
Assert.Equal(_userMock.FirstName, extracted.FirstName);
|
||||||
|
Assert.Equal(_userMock.LastName, extracted.LastName);
|
||||||
|
Assert.Equal(_userMock.Email, extracted.Email);
|
||||||
|
Assert.Equal(_userMock.SecurityStamp, extracted.SecurityStamp);
|
||||||
|
|
||||||
|
var decodedTokenString = Encoding.UTF8.GetString(Convert.FromBase64String(raw));
|
||||||
|
Assert.DoesNotContain("UserRoles", decodedTokenString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Tuitio.Application.Stores;
|
||||||
|
using Tuitio.Domain.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Tuitio.Application.Tests
|
||||||
|
{
|
||||||
|
public class TokenStoreTests
|
||||||
|
{
|
||||||
|
private Token GetMockedToken()
|
||||||
|
{
|
||||||
|
var token = new Token(1);
|
||||||
|
token.SetUserData(0, "test.tuitio", "tuitio", "user", "user.tuitio@lab.com", Guid.NewGuid().ToString(), null, null, null);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Set_ShouldSetTokenInStore()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var key = "user001";
|
||||||
|
var expected = GetMockedToken();
|
||||||
|
|
||||||
|
var store = new TokenStore();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
store.Set(key, expected);
|
||||||
|
var actual = store.Get(key);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Remove_ShouldRemoveTokenFromStore()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var key = "user001";
|
||||||
|
var mock = GetMockedToken();
|
||||||
|
var store = new TokenStore();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
store.Set(key, mock);
|
||||||
|
store.Remove(key);
|
||||||
|
var actual = store.Get(key);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(actual == null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue