diff --git a/.gitea/workflows/BuildLibrary.yml b/.gitea/workflows/BuildLibrary.yml index eb197ed..f26a6ad 100644 --- a/.gitea/workflows/BuildLibrary.yml +++ b/.gitea/workflows/BuildLibrary.yml @@ -44,9 +44,7 @@ jobs: --name gitea \ https://git.netzbyte.com/api/packages/$GITEA_USER/nuget/index.json - # --skip-duplicate - Prevents errors if the package already exists. - # --api-key - An random value to silence the 'No API key found' warning. - dotnet nuget push ./artifacts/*.nupkg --source gitea --skip-duplicate --api-key SILENCE_API_KEY_WARNING + dotnet nuget push ./artifacts/*.nupkg --source gitea --skip-duplicate env: GITEA_USER: max GITEA_TOKEN: ${{ secrets.PACKAGES_TOKEN }} \ No newline at end of file diff --git a/DotBased.ASP.Auth/AuthDataCache.cs b/DotBased.ASP.Auth/AuthDataCache.cs new file mode 100644 index 0000000..76287e4 --- /dev/null +++ b/DotBased.ASP.Auth/AuthDataCache.cs @@ -0,0 +1,98 @@ +using System.Collections.ObjectModel; +using DotBased.ASP.Auth.Domains.Auth; +using Microsoft.AspNetCore.Components.Authorization; + +namespace DotBased.ASP.Auth; + +public class AuthDataCache +{ + public AuthDataCache(BasedAuthConfiguration configuration) + { + _configuration = configuration; + } + + private readonly BasedAuthConfiguration _configuration; + + private readonly AuthStateCacheCollection _authenticationStateCollection = []; + + public Result PurgeSessionState(string id) => _authenticationStateCollection.Remove(id) ? Result.Ok() : Result.Failed("Failed to purge session state from cache! Or the session was not cached..."); + + public void CacheSessionState(AuthenticationStateModel stateModel, AuthenticationState? state = null) => _authenticationStateCollection[stateModel.Id] = + new AuthStateCacheNode(stateModel, state); + + public Result> RequestSessionState(string id) + { + if (!_authenticationStateCollection.TryGetValue(id, out var node)) + return Result>.Failed("No cached object found!"); + string failedMsg; + if (node.StateModel != null) + { + if (node.IsValidLifespan(_configuration.CachedAuthSessionLifespan)) + return Result>.Ok(new Tuple(node.StateModel, node.State)); + failedMsg = $"Session has invalid lifespan, removing entry: [{id}] from cache!"; + } + else + failedMsg = $"Returned object is null, removing entry: [{id}] from cache!"; + _authenticationStateCollection.Remove(id); + return Result>.Failed(failedMsg); + } +} + +public class AuthStateCacheNode where TStateModel : class where TState : class +{ + public AuthStateCacheNode(TStateModel stateModel, TState? state) + { + StateModel = stateModel; + State = state; + } + public TStateModel? StateModel { get; private set; } + public TState? State { get; private set; } + public DateTime DateCached { get; private set; } = DateTime.Now; + + public void UpdateObject(TStateModel obj) + { + StateModel = obj; + DateCached = DateTime.Now; + } + + /// + /// Checks if the cached object is within the given lifespan. + /// + /// The max. lifespan + public bool IsValidLifespan(TimeSpan lifespan) => DateCached.Add(lifespan) > DateTime.Now; + + public override bool Equals(object? obj) + { + if (obj is AuthStateCacheNode cacheObj) + return StateModel != null && StateModel.Equals(cacheObj.StateModel); + return false; + } + + public override int GetHashCode() => typeof(TStateModel).GetHashCode(); + public override string ToString() => typeof(TStateModel).ToString(); +} + +public class AuthStateCacheCollection : KeyedCollection> where TStateModel : class where TState : class +{ + protected override string GetKeyForItem(AuthStateCacheNode item) => item.StateModel?.ToString() ?? string.Empty; + + public new AuthStateCacheNode? this[string id] + { + get => TryGetValue(id, out AuthStateCacheNode? nodeValue) ? nodeValue : null; + set + { + if (value == null) + return; + if (TryGetValue(id, out AuthStateCacheNode? nodeValue)) + Remove(nodeValue); + Add(value); + } + } + + public void Insert(AuthStateCacheNode node) + { + if (Contains(node)) + Remove(node); + Add(node); + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/AuthenticationService.cs b/DotBased.ASP.Auth/AuthenticationService.cs new file mode 100644 index 0000000..9b4d3b2 --- /dev/null +++ b/DotBased.ASP.Auth/AuthenticationService.cs @@ -0,0 +1,13 @@ +namespace DotBased.ASP.Auth.Services; + +public class AuthenticationService +{ + public AuthenticationService() + { + /* + * - Login + * - Logout + * - Register + */ + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/BasedAuthConfiguration.cs b/DotBased.ASP.Auth/BasedAuthConfiguration.cs new file mode 100644 index 0000000..a6871ba --- /dev/null +++ b/DotBased.ASP.Auth/BasedAuthConfiguration.cs @@ -0,0 +1,65 @@ +namespace DotBased.ASP.Auth; + +public class BasedAuthConfiguration +{ + /// + /// Allow users to registrate. + /// + public bool AllowRegistration { get; set; } + //TODO: Callback when a user registers, so the application can handle sending emails or generate a code to complete the registration. + //TODO: Callback for validation email, phone number + /// + /// Allow no passwords on users, not recommended! + /// + public bool AllowEmptyPassword { get; set; } = false; + /// + /// This path is used for redirecting to the login page. + /// + public string LoginPath { get; set; } = string.Empty; + /// + /// The path that will be used if the logout is requested. + /// + public string LogoutPath { get; set; } = string.Empty; + /// + /// The page that the client will be redirected to after logging out. + /// + public string LoggedOutPath { get; set; } = string.Empty; + /// + /// The max age before a AuthenticationState will expire (default: 7 days). + /// + public TimeSpan AuthenticationStateMaxAgeBeforeExpire { get; set; } = TimeSpan.FromDays(7); + /// + /// How long a session state will be cached (default: 15 min) + /// + public TimeSpan CachedAuthSessionLifespan { get; set; } = TimeSpan.FromMinutes(15); + /// + /// Can be used to seed a default user and/or group for first time use. + /// + public Action? SeedData { get; set; } + + public Type? AuthDataRepositoryType { get; private set; } + + public void SetDataRepositoryType() where TDataProviderType : IAuthDataRepository => + AuthDataRepositoryType = typeof(TDataProviderType); + + public Type? SessionStateProviderType { get; private set; } + + public void SetSessionStateProviderType() + where TSessionStateProviderType : ISessionStateProvider => + SessionStateProviderType = typeof(TSessionStateProviderType); +} + +public class BasedPasswordOptions +{ + +} + +public class BasedUserOptions +{ + +} + +public class BasedLockoutOptions +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/BasedAuthDefaults.cs b/DotBased.ASP.Auth/BasedAuthDefaults.cs new file mode 100644 index 0000000..824740b --- /dev/null +++ b/DotBased.ASP.Auth/BasedAuthDefaults.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +namespace DotBased.ASP.Auth; + +public static class BasedAuthDefaults +{ + public const string AuthenticationScheme = "DotBasedAuthentication"; + public const string StorageKey = "dotbased_session"; + + public static IComponentRenderMode InteractiveServerWithoutPrerender { get; } = + new InteractiveServerRenderMode(prerender: false); +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs b/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs new file mode 100644 index 0000000..0863561 --- /dev/null +++ b/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs @@ -0,0 +1,40 @@ +using System.Security.Claims; +using DotBased.Logging; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; +using ILogger = DotBased.Logging.ILogger; + +namespace DotBased.ASP.Auth; + +// RevalidatingServerAuthenticationStateProvider +// AuthenticationStateProvider +// Handles roles +public class BasedServerAuthenticationStateProvider : ServerAuthenticationStateProvider +{ + public BasedServerAuthenticationStateProvider(BasedAuthConfiguration configuration, ProtectedLocalStorage localStorage, SecurityService securityService) + { + _config = configuration; + _localStorage = localStorage; + _securityService = securityService; + _logger = LogService.RegisterLogger(); + } + + private BasedAuthConfiguration _config; + private readonly ProtectedLocalStorage _localStorage; + private readonly SecurityService _securityService; + private readonly ILogger _logger; + private readonly AuthenticationState _anonState = new(new ClaimsPrincipal()); + + + public override async Task GetAuthenticationStateAsync() + { + _logger.Debug("Getting authentication state..."); + var sessionIdResult = await _localStorage.GetAsync(BasedAuthDefaults.StorageKey); + if (!sessionIdResult.Success || sessionIdResult.Value == null) + return _anonState; + _logger.Debug("Found state [{State}], getting session from {Service}", sessionIdResult.Value, nameof(SecurityService)); + var stateResult = await _securityService.GetAuthenticationStateFromSessionAsync(sessionIdResult.Value); + return stateResult is { Success: true, Value: not null } ? stateResult.Value : _anonState; + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs b/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs new file mode 100644 index 0000000..78a5c79 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs @@ -0,0 +1,25 @@ +using DotBased.ASP.Auth.Domains.Identity; + +namespace DotBased.ASP.Auth.Domains.Auth; + +public class AuthenticationStateModel +{ + public AuthenticationStateModel(UserModel user) + { + UserId = user.Id; + } + + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string UserId { get; set; } + public DateTime CreationDate { get; set; } = DateTime.Now; + + public override bool Equals(object? obj) + { + if (obj is AuthenticationStateModel authStateModel) + return authStateModel.Id == Id; + return false; + } + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => Id.GetHashCode(); + public override string ToString() => Id; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Auth/PermissionModel.cs b/DotBased.ASP.Auth/Domains/Auth/PermissionModel.cs new file mode 100644 index 0000000..93353fe --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Auth/PermissionModel.cs @@ -0,0 +1,8 @@ +namespace DotBased.ASP.Auth.Domains.Auth; + +public class PermissionModel +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Permission { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Auth/RoleModel.cs b/DotBased.ASP.Auth/Domains/Auth/RoleModel.cs new file mode 100644 index 0000000..92b15c7 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Auth/RoleModel.cs @@ -0,0 +1,12 @@ +using DotBased.Objects; + +namespace DotBased.ASP.Auth.Domains.Auth; + +public class RoleModel +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List Permissions { get; set; } = []; + public List> Attributes { get; set; } = []; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Identity/GroupItemModel.cs b/DotBased.ASP.Auth/Domains/Identity/GroupItemModel.cs new file mode 100644 index 0000000..30508d8 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Identity/GroupItemModel.cs @@ -0,0 +1,8 @@ +namespace DotBased.ASP.Auth.Domains.Identity; + +public class GroupItemModel +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Identity/GroupModel.cs b/DotBased.ASP.Auth/Domains/Identity/GroupModel.cs new file mode 100644 index 0000000..73ff377 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Identity/GroupModel.cs @@ -0,0 +1,13 @@ +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.Objects; + +namespace DotBased.ASP.Auth.Domains.Identity; + +public class GroupModel +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List Roles { get; set; } = []; + public List> Attributes { get; set; } = []; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Identity/UserItemModel.cs b/DotBased.ASP.Auth/Domains/Identity/UserItemModel.cs new file mode 100644 index 0000000..1d07566 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Identity/UserItemModel.cs @@ -0,0 +1,10 @@ +namespace DotBased.ASP.Auth.Domains.Identity; + +public class UserItemModel +{ + public string Id { get; set; } = string.Empty; + public string UserName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string FamilyName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Identity/UserModel.cs b/DotBased.ASP.Auth/Domains/Identity/UserModel.cs new file mode 100644 index 0000000..b9a9f51 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Identity/UserModel.cs @@ -0,0 +1,31 @@ +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.Objects; + +namespace DotBased.ASP.Auth.Domains.Identity; + +public class UserModel +{ + public string UserName { get; set; } = string.Empty; + public string PasswordHash { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string PhoneNumber { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string FamilyName { get; set; } = string.Empty; + public DateTime Dob { get; set; } + + public string Id { get; set; } = Guid.NewGuid().ToString(); + public bool Enabled { get; set; } + public bool EmailValidated { get; set; } + public bool PhoneNumberConfirmed { get; set; } + public bool Lockout { get; set; } + public DateTime LockoutEnd { get; set; } + public DateTime CreationStamp { get; set; } + public DateTime SecurityStamp { get; set; } + public DateTime ConcurrencyStamp { get; set; } + public int AccessFailedCount { get; set; } + public bool ExternalAuthentication { get; set; } + + public List Groups { get; set; } = []; + public List Roles { get; set; } = []; + public List> Attributes { get; set; } = []; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/LoginModel.cs b/DotBased.ASP.Auth/Domains/LoginModel.cs new file mode 100644 index 0000000..a011eb6 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/LoginModel.cs @@ -0,0 +1,8 @@ +namespace DotBased.ASP.Auth.Domains; + +public class LoginModel +{ + public string UserName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/RegisterModel.cs b/DotBased.ASP.Auth/Domains/RegisterModel.cs new file mode 100644 index 0000000..877476c --- /dev/null +++ b/DotBased.ASP.Auth/Domains/RegisterModel.cs @@ -0,0 +1,10 @@ +namespace DotBased.ASP.Auth.Domains; + +public class RegisterModel +{ + public string UserName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string FamilyName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/DotBased.ASP.Auth.csproj b/DotBased.ASP.Auth/DotBased.ASP.Auth.csproj new file mode 100644 index 0000000..e51c97a --- /dev/null +++ b/DotBased.ASP.Auth/DotBased.ASP.Auth.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs b/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs new file mode 100644 index 0000000..59fa9e0 --- /dev/null +++ b/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs @@ -0,0 +1,55 @@ +using DotBased.ASP.Auth.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; + +namespace DotBased.ASP.Auth; + +public static class DotBasedAuthDependencyInjection +{ + /// + /// Use the DotBased authentication implementation + /// + /// Use UseBasedServerAuth()! + /// Service collection + /// DotBased auth configuration + public static IServiceCollection AddBasedServerAuth(this IServiceCollection services, Action? configurationAction = null) + { + var Configuration = new BasedAuthConfiguration(); + configurationAction?.Invoke(Configuration); + + services.AddSingleton(Configuration); + if (Configuration.AuthDataRepositoryType == null) + throw new ArgumentNullException(nameof(Configuration.AuthDataRepositoryType), $"No '{nameof(IAuthDataRepository)}' configured!"); + services.AddScoped(typeof(IAuthDataRepository), Configuration.AuthDataRepositoryType); + + services.AddSingleton(); + services.AddScoped(); + + services.AddScoped(); + services.AddAuthentication(options => + { + options.DefaultScheme = BasedAuthDefaults.AuthenticationScheme; + });/*.AddScheme(BasedAuthDefaults.AuthenticationScheme, null);*/ + services.AddAuthorization(); + services.AddCascadingAuthenticationState(); + return services; + } + + public static WebApplication UseBasedServerAuth(this WebApplication app) + { + app.UseAuthentication(); + app.UseAuthorization(); + + // Data + var authConfig = app.Services.GetService(); + if (authConfig == null) + throw new NullReferenceException($"{nameof(BasedAuthConfiguration)} is null!"); + if (authConfig.AuthDataRepositoryType == null) + throw new NullReferenceException($"{nameof(authConfig.AuthDataRepositoryType)} is null, cannot instantiate an instance of {nameof(IAuthDataRepository)}"); + var dataProvider = (IAuthDataRepository?)Activator.CreateInstance(authConfig.AuthDataRepositoryType); + if (dataProvider != null) authConfig.SeedData?.Invoke(dataProvider); + + return app; + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/IAuthDataRepository.cs b/DotBased.ASP.Auth/IAuthDataRepository.cs new file mode 100644 index 0000000..f1ed79c --- /dev/null +++ b/DotBased.ASP.Auth/IAuthDataRepository.cs @@ -0,0 +1,22 @@ +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.ASP.Auth.Domains.Identity; + +namespace DotBased.ASP.Auth; + +public interface IAuthDataRepository +{ + public Task CreateUserAsync(UserModel user); + public Task UpdateUserAsync(UserModel user); + public Task DeleteUserAsync(UserModel user); + public Task> GetUserAsync(string id, string email, string username); + public Task> GetUsersAsync(int start = 0, int amount = 30, string search = ""); + public Task CreateGroupAsync(GroupModel group); + public Task UpdateGroupAsync(GroupModel group); + public Task DeleteGroupAsync(GroupModel group); + public Task> GetGroupAsync(string id); + public Task> GetGroupsAsync(int start = 0, int amount = 30, string search = ""); + public Task CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState); + public Task UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState); + public Task DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState); + public Task> GetAuthenticationStateAsync(string id); +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/ISessionStateProvider.cs b/DotBased.ASP.Auth/ISessionStateProvider.cs new file mode 100644 index 0000000..ee8850a --- /dev/null +++ b/DotBased.ASP.Auth/ISessionStateProvider.cs @@ -0,0 +1,8 @@ +namespace DotBased.ASP.Auth; + +public interface ISessionStateProvider +{ + public const string SessionStateName = "BasedServerSession"; + public Task> GetSessionStateAsync(); + public Task SetSessionStateAsync(string state); +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/MemoryAuthDataRepository.cs b/DotBased.ASP.Auth/MemoryAuthDataRepository.cs new file mode 100644 index 0000000..708db7a --- /dev/null +++ b/DotBased.ASP.Auth/MemoryAuthDataRepository.cs @@ -0,0 +1,107 @@ +using System.Diagnostics.CodeAnalysis; +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.ASP.Auth.Domains.Identity; +using DotBased.Extensions; + +namespace DotBased.ASP.Auth; +/// +/// In memory data provider, for testing only! +/// +[SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] +public class MemoryAuthDataRepository : IAuthDataRepository +{ + public async Task CreateUserAsync(UserModel user) + { + if (MemoryData.users.Any(x => x.Id == user.Id || x.Email == user.Email)) + return Result.Failed("User already exists."); + MemoryData.users.Add(user); + return Result.Ok(); + } + + public async Task UpdateUserAsync(UserModel user) + { + if (MemoryData.users.All(x => x.Id != user.Id)) + return Result.Failed("User does not exist!"); + + return Result.Ok(); + } + + public Task DeleteUserAsync(UserModel user) + { + throw new NotImplementedException(); + } + + public async Task> GetUserAsync(string id, string email, string username) + { + UserModel? userModel = null; + if (!id.IsNullOrEmpty()) + userModel = MemoryData.users.FirstOrDefault(u => u.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); + if (!email.IsNullOrEmpty()) + userModel = MemoryData.users.FirstOrDefault(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase)); + if (!username.IsNullOrEmpty()) + userModel = MemoryData.users.FirstOrDefault(u => u.UserName.Equals(username, StringComparison.OrdinalIgnoreCase)); + return userModel != null ? Result.Ok(userModel) : Result.Failed("No user found!"); + } + + public Task> GetUsersAsync(int start = 0, int amount = 30, string search = "") + { + throw new NotImplementedException(); + } + + public Task CreateGroupAsync(GroupModel group) + { + throw new NotImplementedException(); + } + + public Task UpdateGroupAsync(GroupModel group) + { + throw new NotImplementedException(); + } + + public Task DeleteGroupAsync(GroupModel group) + { + throw new NotImplementedException(); + } + + public Task> GetGroupAsync(string id) + { + throw new NotImplementedException(); + } + + public Task> GetGroupsAsync(int start = 0, int amount = 30, string search = "") + { + throw new NotImplementedException(); + } + + public async Task CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState) + { + if (MemoryData.AuthenticationStates.Contains(authenticationState)) return Result.Failed("Item already exists!"); + MemoryData.AuthenticationStates.Add(authenticationState); + return Result.Ok(); + } + + public Task UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState) + { + throw new NotImplementedException(); + } + + public async Task DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState) + { + MemoryData.AuthenticationStates.Remove(authenticationState); + return Result.Ok(); + } + + public async Task> GetAuthenticationStateAsync(string id) + { + var item = MemoryData.AuthenticationStates.FirstOrDefault(x => x.Id == id); + if (item == null) return Result.Failed("Could not get the session state!"); + return Result.Ok(item); + } +} + +internal static class MemoryData +{ + public static readonly List users = []; + public static readonly List Groups = []; + public static readonly List AuthenticationStates = []; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/AuthConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/AuthConfiguration.cs new file mode 100644 index 0000000..2bbf690 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/AuthConfiguration.cs @@ -0,0 +1,11 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class AuthConfiguration +{ + public CacheConfiguration Cache { get; set; } = new(); + public LockoutConfiguration Lockout { get; set; } = new(); + public PasswordConfiguration Password { get; set; } = new(); + public ProviderConfiguration Provider { get; set; } = new(); + public RepositoryConfiguration Repository { get; set; } = new(); + public UserConfiguration User { get; set; } = new(); +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/CacheConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/CacheConfiguration.cs new file mode 100644 index 0000000..8647941 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/CacheConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class CacheConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/LockoutConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/LockoutConfiguration.cs new file mode 100644 index 0000000..b59ae65 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/LockoutConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class LockoutConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/PasswordConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/PasswordConfiguration.cs new file mode 100644 index 0000000..c590cdd --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/PasswordConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class PasswordConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/ProviderConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/ProviderConfiguration.cs new file mode 100644 index 0000000..cf3f702 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/ProviderConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class ProviderConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/RepositoryConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/RepositoryConfiguration.cs new file mode 100644 index 0000000..cb55903 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/RepositoryConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class RepositoryConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/UserConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/UserConfiguration.cs new file mode 100644 index 0000000..a4dd082 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/UserConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class UserConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/SecurityManager.cs b/DotBased.ASP.Auth/SecurityManager.cs new file mode 100644 index 0000000..0067eae --- /dev/null +++ b/DotBased.ASP.Auth/SecurityManager.cs @@ -0,0 +1,9 @@ +namespace DotBased.ASP.Auth.Managers; + +public class SecurityManager +{ + public SecurityManager() + { + + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/SecurityService.cs b/DotBased.ASP.Auth/SecurityService.cs new file mode 100644 index 0000000..748e0a7 --- /dev/null +++ b/DotBased.ASP.Auth/SecurityService.cs @@ -0,0 +1,137 @@ +using System.Security.Claims; +using DotBased.ASP.Auth.Domains; +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.ASP.Auth.Domains.Identity; +using DotBased.Extensions; +using DotBased.Logging; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; + +namespace DotBased.ASP.Auth; + +public class SecurityService +{ + public SecurityService(IAuthDataRepository authDataRepository, AuthDataCache dataCache, ProtectedLocalStorage localStorage) + { + _authDataRepository = authDataRepository; + _dataCache = dataCache; + _localStorage = localStorage; + _logger = LogService.RegisterLogger(); + } + + private readonly IAuthDataRepository _authDataRepository; + private readonly AuthDataCache _dataCache; + private readonly ProtectedLocalStorage _localStorage; + private readonly ILogger _logger; + + public async Task> GetAuthenticationStateFromSessionAsync(string id) + { + if (id.IsNullOrEmpty()) + return Result.Failed("No valid id!"); + AuthenticationStateModel? authStateModel = null; + var stateCache = _dataCache.RequestSessionState(id); + if (!stateCache.Success || stateCache.Value == null) + { + var stateResult = await _authDataRepository.GetAuthenticationStateAsync(id); + if (stateResult is { Success: true, Value: not null }) + { + authStateModel = stateResult.Value; + _dataCache.CacheSessionState(authStateModel); + } + } + else + { + if (stateCache.Value.Item2 != null) + return Result.Ok(stateCache.Value.Item2); + authStateModel = stateCache.Value.Item1; + } + + if (authStateModel == null) + return Result.Failed("Failed to get auth state!"); + + var userResult = await _authDataRepository.GetUserAsync(authStateModel.UserId, string.Empty, string.Empty); + if (userResult is not { Success: true, Value: not null }) + return Result.Failed("Failed to get user from state!"); + var claims = new List() + { + new(ClaimTypes.Sid, userResult.Value.Id), + new(ClaimTypes.Name, userResult.Value.Name), + new(ClaimTypes.NameIdentifier, userResult.Value.UserName), + new(ClaimTypes.Surname, userResult.Value.FamilyName), + new(ClaimTypes.Email, userResult.Value.Email) + }; + claims.AddRange(userResult.Value.Groups.Select(group => new Claim(ClaimTypes.GroupSid, group.Id))); + claims.AddRange(userResult.Value.Roles.Select(role => new Claim(ClaimTypes.Role, role.Name))); + claims.AddRange(userResult.Value.Groups.Select(g => g.Roles).SelectMany(gRolesList => gRolesList, (_, role) => new Claim(ClaimTypes.Role, role.Name))); + var claimsIdentity = new ClaimsIdentity(claims, BasedAuthDefaults.AuthenticationScheme); + var authState = new AuthenticationState(new ClaimsPrincipal(claimsIdentity)); + _dataCache.CacheSessionState(authStateModel, authState); + return Result.Ok(authState); + } + + public async Task> LoginAsync(LoginModel login) + { + try + { + UserModel? user = null; + Result usrResult; + if (!login.UserName.IsNullOrEmpty()) + { + usrResult = await _authDataRepository.GetUserAsync(string.Empty, string.Empty, login.UserName); + if (usrResult is { Success: true, Value: not null }) + user = usrResult.Value; + } + else if (!login.Email.IsNullOrEmpty()) + { + usrResult = await _authDataRepository.GetUserAsync(string.Empty, login.Email, string.Empty); + if (usrResult is { Success: true, Value: not null }) + user = usrResult.Value; + } + else + return Result.Failed("Username & Email is empty, cannot login!"); + + if (user == null || !usrResult.Success) + return Result.Failed("No user found!"); + + if (user.PasswordHash != login.Password) //TODO: Hash password and compare + return Result.Failed("Login failed, invalid password."); + var state = new AuthenticationStateModel(user); + var authResult = await _authDataRepository.CreateAuthenticationStateAsync(state); + if (!authResult.Success) + return Result.Failed("Failed to store session to database!"); + _dataCache.CacheSessionState(state); + await _localStorage.SetAsync(BasedAuthDefaults.StorageKey, state.Id); + return Result.Ok(state); + } + catch (Exception e) + { + _logger.Error(e, "Failed to login!"); + return Result.Failed("Login failed, exception thrown!"); + } + } + + public async Task LogoutAsync(string state) + { + try + { + if (state.IsNullOrEmpty()) + return Result.Failed($"Argument {nameof(state)} is empty!"); + + var stateResult = await _authDataRepository.GetAuthenticationStateAsync(state); + if (!stateResult.Success || stateResult.Value == null) + return stateResult; + var authState = stateResult.Value; + + _dataCache.PurgeSessionState(state); + var updatedStateResult = await _authDataRepository.DeleteAuthenticationStateAsync(authState); + if (updatedStateResult.Success) return updatedStateResult; + _logger.Warning(updatedStateResult.Message); + return updatedStateResult; + } + catch (Exception e) + { + _logger.Error(e, "Failed to logout!"); + return Result.Failed("Failed to logout, exception thrown!"); + } + } +} \ No newline at end of file diff --git a/DotBased.AspNet.Auth/BasedAuthExtensions.cs b/DotBased.AspNet.Auth/BasedAuthExtensions.cs new file mode 100644 index 0000000..023a35f --- /dev/null +++ b/DotBased.AspNet.Auth/BasedAuthExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace DotBased.AspNet.Auth; + +public static class BasedAuthExtensions +{ + public static IServiceCollection AddBasedAuthentication(this IServiceCollection services) + { + return services; + } +} \ No newline at end of file diff --git a/DotBased.AspNet.Auth/DotBased.AspNet.Auth.csproj b/DotBased.AspNet.Auth/DotBased.AspNet.Auth.csproj new file mode 100644 index 0000000..3af943e --- /dev/null +++ b/DotBased.AspNet.Auth/DotBased.AspNet.Auth.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + ..\..\..\..\..\usr\lib64\dotnet\shared\Microsoft.AspNetCore.App\8.0.11\Microsoft.AspNetCore.Authentication.dll + + + ..\..\..\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\8.0.2\lib\net8.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + + + + + + diff --git a/DotBased.Logging.MEL/BasedLogger.cs b/DotBased.Logging.MEL/BasedLogger.cs index 755aeb4..39574a1 100644 --- a/DotBased.Logging.MEL/BasedLogger.cs +++ b/DotBased.Logging.MEL/BasedLogger.cs @@ -4,13 +4,13 @@ namespace DotBased.Logging.MEL; public class BasedLogger : Microsoft.Extensions.Logging.ILogger { - private const string MessageTemplateKey = "{OriginalFormat}"; + private const string _messageTemplateKey = "{OriginalFormat}"; public BasedLogger(ILogger logger) { - _basedLogger = logger; + basedLogger = logger; } - private readonly ILogger _basedLogger; + private readonly ILogger basedLogger; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { @@ -18,7 +18,7 @@ public class BasedLogger : Microsoft.Extensions.Logging.ILogger return; var severity = ConvertLogLevelToSeverity(logLevel); var capsule = ConstructCapsule(severity, eventId, state, exception, formatter); - _basedLogger.Log(capsule); + basedLogger.Log(capsule); } private LogCapsule ConstructCapsule(LogSeverity severity, EventId eventId, TState state, Exception? exception, Func formatter) @@ -29,7 +29,7 @@ public class BasedLogger : Microsoft.Extensions.Logging.ILogger { foreach (var prop in stateEnum) { - if (prop is { Key: MessageTemplateKey, Value: string propValueString }) + if (prop is { Key: _messageTemplateKey, Value: string propValueString }) { msgTemplate = propValueString; continue; @@ -37,8 +37,16 @@ public class BasedLogger : Microsoft.Extensions.Logging.ILogger templateParams.Add(prop.Value); } } - - return new LogCapsule(_basedLogger as LoggerBase ?? throw new NullReferenceException(nameof(_basedLogger)), severity, msgTemplate, exception, templateParams.ToArray(), DateTime.Now); + + return new LogCapsule() + { + Exception = exception, + Message = msgTemplate, + Parameters = templateParams.ToArray(), + Severity = severity, + TimeStamp = DateTime.Now, + Logger = basedLogger as LoggerBase ?? throw new NullReferenceException(nameof(basedLogger)) + }; } private LogSeverity ConvertLogLevelToSeverity(LogLevel level) @@ -58,5 +66,5 @@ public class BasedLogger : Microsoft.Extensions.Logging.ILogger public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; - public IDisposable? BeginScope(TState state) where TState : notnull => null; + public IDisposable? BeginScope(TState state) where TState : notnull => default; } \ No newline at end of file diff --git a/DotBased.Logging.MEL/DotBased.Logging.MEL.csproj b/DotBased.Logging.MEL/DotBased.Logging.MEL.csproj index e259c75..9b5d862 100644 --- a/DotBased.Logging.MEL/DotBased.Logging.MEL.csproj +++ b/DotBased.Logging.MEL/DotBased.Logging.MEL.csproj @@ -1,13 +1,10 @@  - net8.0 + netstandard2.1 enable enable 12 - NetzByte - https://git.netzbyte.com/max/DotBased - https://git.netzbyte.com/max/DotBased diff --git a/DotBased.Logging.Serilog/DotBased.Logging.Serilog.csproj b/DotBased.Logging.Serilog/DotBased.Logging.Serilog.csproj index 82eafe3..e43e411 100755 --- a/DotBased.Logging.Serilog/DotBased.Logging.Serilog.csproj +++ b/DotBased.Logging.Serilog/DotBased.Logging.Serilog.csproj @@ -1,12 +1,9 @@ - net8.0 + netstandard2.1 enable default - NetzByte - https://git.netzbyte.com/max/DotBased - https://git.netzbyte.com/max/DotBased diff --git a/DotBased.sln b/DotBased.sln index 00949e9..4221777 100755 --- a/DotBased.sln +++ b/DotBased.sln @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.Logging.Serilog", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{2156FB93-C252-4B33-8A0C-73C82FABB163}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.ASP.Auth", "DotBased.ASP.Auth\DotBased.ASP.Auth.csproj", "{CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.Logging.MEL", "DotBased.Logging.MEL\DotBased.Logging.MEL.csproj", "{D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{DBDB4538-85D4-45AC-9E0A-A684467AEABA}" @@ -16,6 +18,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWebApi", "TestWebApi\Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazor.Wasm", "Blazor.Wasm\Blazor.Wasm.csproj", "{AC8343A5-7953-4E1D-A926-406BE4D7E819}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNet", "AspNet", "{624E7B11-8A18-46E5-AB1F-6AF6097F9D4D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.AspNet.Auth", "DotBased.AspNet.Auth\DotBased.AspNet.Auth.csproj", "{6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,6 +40,10 @@ Global {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC}.Release|Any CPU.Build.0 = Release|Any CPU + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Release|Any CPU.Build.0 = Release|Any CPU {D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -46,11 +56,18 @@ Global {AC8343A5-7953-4E1D-A926-406BE4D7E819}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC8343A5-7953-4E1D-A926-406BE4D7E819}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC8343A5-7953-4E1D-A926-406BE4D7E819}.Release|Any CPU.Build.0 = Release|Any CPU + {6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC} = {2156FB93-C252-4B33-8A0C-73C82FABB163} + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D} = {2156FB93-C252-4B33-8A0C-73C82FABB163} {D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D} = {2156FB93-C252-4B33-8A0C-73C82FABB163} {BADA4BAF-142B-47A8-95FC-B25E1D3D0020} = {DBDB4538-85D4-45AC-9E0A-A684467AEABA} {AC8343A5-7953-4E1D-A926-406BE4D7E819} = {DBDB4538-85D4-45AC-9E0A-A684467AEABA} + {624E7B11-8A18-46E5-AB1F-6AF6097F9D4D} = {2156FB93-C252-4B33-8A0C-73C82FABB163} + {6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B} = {624E7B11-8A18-46E5-AB1F-6AF6097F9D4D} EndGlobalSection EndGlobal diff --git a/DotBased/DotBased-ICO.png b/DotBased/DotBased-ICO.png deleted file mode 100644 index c207cd2..0000000 Binary files a/DotBased/DotBased-ICO.png and /dev/null differ diff --git a/DotBased/DotBased.csproj b/DotBased/DotBased.csproj index a3649e6..abda654 100755 --- a/DotBased/DotBased.csproj +++ b/DotBased/DotBased.csproj @@ -4,18 +4,7 @@ enable enable default - net8.0 - NetzByte - DotBased-ICO.png - https://git.netzbyte.com/max/DotBased - https://git.netzbyte.com/max/DotBased + netstandard2.1 - - - True - - - - diff --git a/DotBased/Logging/LogCapsule.cs b/DotBased/Logging/LogCapsule.cs index 8f997cc..4c8e423 100755 --- a/DotBased/Logging/LogCapsule.cs +++ b/DotBased/Logging/LogCapsule.cs @@ -3,4 +3,21 @@ namespace DotBased.Logging; /// /// This will contain all the log event information that the log adapter will receive. /// -public record LogCapsule(LoggerBase Logger, LogSeverity Severity, string Message, Exception? Exception, object?[]? Parameters, DateTime TimeStamp); \ No newline at end of file +public class LogCapsule +{ + /// + /// The log serverty this log event is being logged. + /// + public LogSeverity Severity { get; set; } + public string Message { get; set; } = string.Empty; + public Exception? Exception { get; set; } + public object?[]? Parameters { get; set; } + /// + /// Time stamp on when this event happend + /// + public DateTime TimeStamp { get; set; } + /// + /// The logger that generated this capsule + /// + public LoggerBase Logger { get; set; } +} \ No newline at end of file diff --git a/DotBased/Logging/Logger.cs b/DotBased/Logging/Logger.cs index 9480070..79db121 100755 --- a/DotBased/Logging/Logger.cs +++ b/DotBased/Logging/Logger.cs @@ -7,37 +7,88 @@ public class Logger(LoggerInformation loggerInformation, string name) : LoggerBa { public override void Verbose(string message, params object?[]? parameters) { - Log(new LogCapsule(this, LogSeverity.Verbose, message, null, parameters, DateTime.Now)); + Log(new LogCapsule() + { + Logger = this, + Message = message, + Parameters = parameters, + Severity = LogSeverity.Verbose, + TimeStamp = DateTime.Now + }); } public override void Trace(string message, params object?[]? parameters) { - Log(new LogCapsule(this, LogSeverity.Trace, message, null, parameters, DateTime.Now)); + Log(new LogCapsule() + { + Logger = this, + Message = message, + Parameters = parameters, + Severity = LogSeverity.Trace, + TimeStamp = DateTime.Now + }); } public override void Debug(string message, params object?[]? parameters) { - Log(new LogCapsule(this, LogSeverity.Debug, message, null, parameters, DateTime.Now)); + Log(new LogCapsule() + { + Logger = this, + Message = message, + Parameters = parameters, + Severity = LogSeverity.Debug, + TimeStamp = DateTime.Now + }); } public override void Information(string message, params object?[]? parameters) { - Log(new LogCapsule(this, LogSeverity.Info, message, null, parameters, DateTime.Now)); + Log(new LogCapsule() + { + Logger = this, + Message = message, + Parameters = parameters, + Severity = LogSeverity.Info, + TimeStamp = DateTime.Now + }); } public override void Warning(string message, params object?[]? parameters) { - Log(new LogCapsule(this, LogSeverity.Warning, message, null, parameters, DateTime.Now)); + Log(new LogCapsule() + { + Logger = this, + Message = message, + Parameters = parameters, + Severity = LogSeverity.Warning, + TimeStamp = DateTime.Now + }); } public override void Error(Exception exception, string message, params object?[]? parameters) { - Log(new LogCapsule(this, LogSeverity.Error, message, null, parameters, DateTime.Now)); + Log(new LogCapsule() + { + Logger = this, + Message = message, + Parameters = parameters, + Severity = LogSeverity.Error, + TimeStamp = DateTime.Now, + Exception = exception + }); } public override void Fatal(Exception exception, string message, params object?[]? parameters) { - Log(new LogCapsule(this, LogSeverity.Fatal, message, null, parameters, DateTime.Now)); + Log(new LogCapsule() + { + Logger = this, + Message = message, + Parameters = parameters, + Severity = LogSeverity.Fatal, + TimeStamp = DateTime.Now, + Exception = exception + }); } public override int GetHashCode() => HashCode.Combine(LoggerInformation.TypeFullName, LoggerInformation.AssemblyFullname); diff --git a/DotBased/Monads/Result.cs b/DotBased/Monads/Result.cs deleted file mode 100644 index c36423d..0000000 --- a/DotBased/Monads/Result.cs +++ /dev/null @@ -1,93 +0,0 @@ -namespace DotBased.Monads; - -public class Result -{ - protected Result() - { - IsSuccess = true; - } - - protected Result(Exception exception) - { - IsSuccess = false; - Error = ResultError.Error(exception); - } - - protected Result(ResultError error) - { - IsSuccess = false; - Error = error; - } - - public bool IsSuccess { get; } - public ResultError? Error { get; set; } - - public static implicit operator Result(Exception exception) => new(exception); - public static implicit operator Result(ResultError error) => new(error); - - public static Result Success() => new(); - public static Result Fail(ResultError error) => new(error); - public static Result Exception(Exception exception) => new(exception); - - - public TMatch Match(Func success, Func failure) => IsSuccess ? success() : failure(Error!); - - public void Match(Action success, Action failure) - { - if (IsSuccess) - { - success(); - } - else - { - failure(Error!); - } - } -} - -public class Result : Result -{ - protected Result(TResult result) - { - _result = result; - } - - protected Result(Exception exception) : base(exception) - { - _result = default; - } - - protected Result(ResultError error) : base(error) - { - _result = default; - } - - private readonly TResult? _result; - public TResult Value => IsSuccess ? _result! : throw new InvalidOperationException("Result is invalid"); - - public static implicit operator Result(TResult result) => new(result); - public static implicit operator Result(Exception exception) => new(exception); - public static implicit operator Result(ResultError error) => new(error); - - public static Result Success(TResult result) => new(result); - public new static Result Fail(ResultError error) => new(error); - public new static Result Exception(Exception exception) => new(exception); - - public TMatch Match(Func success, Func failure) => - IsSuccess && Value != null ? success(Value) : failure(Error ?? ResultError.Fail("No error and value is null!")); -} - -public class ResultError -{ - private ResultError(string description, Exception? exception) - { - Description = description; - Exception = exception; - } - - public string Description { get; } - public Exception? Exception { get; } - - public static ResultError Fail(string description) => new(description, null); - public static ResultError Error(Exception exception, string description = "") => new(description, exception); -} \ No newline at end of file diff --git a/DotBased/Result.cs b/DotBased/Result.cs new file mode 100755 index 0000000..49e8ba6 --- /dev/null +++ b/DotBased/Result.cs @@ -0,0 +1,77 @@ +namespace DotBased; + +/// +/// Simple result class for returning a result state or a message and an exception. +/// +public class Result +{ + public Result(bool success, string message, Exception? exception) + { + Success = success; + Message = message; + Exception = exception; + } + + public Result(Result bObj) + { + Success = bObj.Success; + Message = bObj.Message; + Exception = bObj.Exception; + } + + public bool Success { get; set; } + public string Message { get; set; } + public Exception? Exception { get; set; } + + public static Result Ok() => new(true, string.Empty, null); + public static Result Failed(string message, Exception? exception = null) => new(false, message, exception); +} + +public class Result : Result +{ + public Result(bool success, string message, TValue? value, Exception? exception) : base(success, message, exception) + { + Value = value; + } + public Result(Result bObj) : base(bObj) + { + + } + public TValue? Value { get; set; } + + public static Result Ok(TValue value) => new(true, string.Empty, value, null); + + public new static Result Failed(string message, Exception? exception = null) => + new(false, message, default, exception); +} + +public class ListResult : Result +{ + public ListResult(bool success, string message, int totalCount, IEnumerable? items, Exception? exception) : base(success, message, exception) + { + Items = items != null ? new List(items) : new List(); + TotalCount = totalCount; + } + + public ListResult(Result bObj) : base(bObj) + { + Items = new List(); + } + + public readonly IReadOnlyList Items; + /// + /// The amount of items that this result contains. + /// + public int Count => Items.Count; + + /// + /// The total amount of item that is available. + /// + public int TotalCount { get; } + + public static ListResult Ok(IEnumerable items, int totalCount = -1) => + new(true, string.Empty, totalCount, items, null); + + public new static ListResult Failed(string message, Exception? exception = null) => + new(false, message, -1, null, exception); +} \ No newline at end of file diff --git a/DotBased/Utilities/Cryptography.cs b/DotBased/Utilities/Cryptography.cs index 31cc651..f0e6192 100755 --- a/DotBased/Utilities/Cryptography.cs +++ b/DotBased/Utilities/Cryptography.cs @@ -1,5 +1,4 @@ using System.Security.Cryptography; -using DotBased.Monads; namespace DotBased.Utilities; @@ -13,7 +12,7 @@ public static class Cryptography var outputStream = new StringWriter(); var parameters = csp.ExportParameters(false); if (parameters.Exponent == null || parameters.Modulus == null) - return ResultError.Fail("RSAParameters are empty!"); + return Result.Failed("RSAParameters are empty!"); using (var stream = new MemoryStream()) { var writer = new BinaryWriter(stream); @@ -24,7 +23,7 @@ public static class Cryptography innerWriter.Write((byte)0x30); // SEQUENCE EncodeLength(innerWriter, 13); innerWriter.Write((byte)0x06); // OBJECT IDENTIFIER - var rsaEncryptionOid = new byte[] { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 }; + byte[] rsaEncryptionOid = new byte[] { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 }; EncodeLength(innerWriter, rsaEncryptionOid.Length); innerWriter.Write(rsaEncryptionOid); innerWriter.Write((byte)0x05); // NULL @@ -45,20 +44,20 @@ public static class Cryptography bitStringWriter.Write(paramsStream.GetBuffer(), 0, paramsLength); } - var bitStringLength = (int)bitStringStream.Length; + int bitStringLength = (int)bitStringStream.Length; EncodeLength(innerWriter, bitStringLength); innerWriter.Write(bitStringStream.GetBuffer(), 0, bitStringLength); } - var length = (int)innerStream.Length; + int length = (int)innerStream.Length; EncodeLength(writer, length); writer.Write(innerStream.GetBuffer(), 0, length); } - var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); + char[] base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray(); // WriteLine terminates with \r\n, we want only \n outputStream.Write("-----BEGIN PUBLIC KEY-----\n"); - for (var i = 0; i < base64.Length; i += 64) + for (int i = 0; i < base64.Length; i += 64) { outputStream.Write(base64, i, Math.Min(64, base64.Length - i)); outputStream.Write("\n"); @@ -67,7 +66,7 @@ public static class Cryptography outputStream.Write("-----END PUBLIC KEY-----"); } - return outputStream.ToString(); + return Result.Ok(outputStream.ToString()); } private static void EncodeLength(BinaryWriter stream, int length) @@ -83,15 +82,15 @@ public static class Cryptography default: { // Long form - var temp = length; - var bytesRequired = 0; + int temp = length; + int bytesRequired = 0; while (temp > 0) { temp >>= 8; bytesRequired++; } stream.Write((byte)(bytesRequired | 0x80)); - for (var i = bytesRequired - 1; i >= 0; i--) + for (int i = bytesRequired - 1; i >= 0; i--) { stream.Write((byte)(length >> (8 * i) & 0xff)); } @@ -103,7 +102,7 @@ public static class Cryptography private static void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true) { stream.Write((byte)0x02); // INTEGER - var prefixZeros = value.TakeWhile(t => t == 0).Count(); + int prefixZeros = value.TakeWhile(t => t == 0).Count(); if (value.Length - prefixZeros == 0) { EncodeLength(stream, 1); @@ -121,7 +120,7 @@ public static class Cryptography { EncodeLength(stream, value.Length - prefixZeros); } - for (var i = prefixZeros; i < value.Length; i++) + for (int i = prefixZeros; i < value.Length; i++) { stream.Write(value[i]); } diff --git a/DotBased/Utilities/Culture.cs b/DotBased/Utilities/Culture.cs index 643fa6e..7411011 100755 --- a/DotBased/Utilities/Culture.cs +++ b/DotBased/Utilities/Culture.cs @@ -5,9 +5,9 @@ namespace DotBased.Utilities; public static class Culture { - private static List _sysCultures = []; - private static readonly Dictionary Regions = new(); - private static readonly ILogger Logger = LogService.RegisterLogger(typeof(Culture)); + private static List _sysCultures = new List(); + private static Dictionary _regions = new Dictionary(); + private static readonly ILogger _logger = LogService.RegisterLogger(typeof(Culture)); /// /// Get all system known cultures. @@ -16,7 +16,7 @@ public static class Culture /// The list with 's the system knows public static IEnumerable GetSystemCultures() { - Logger.Debug("Getting system cultures..."); + _logger.Debug("Getting system cultures..."); if (_sysCultures.Count == 0) _sysCultures = CultureInfo.GetCultures(CultureTypes.AllCultures).ToList(); return _sysCultures; @@ -29,16 +29,16 @@ public static class Culture /// A list with regions from the system public static Dictionary GetRegions() { - if (Regions.Count == 0) + if (_regions.Count == 0) { var cultureInfos = GetSystemCultures().Where(cul => !cul.IsNeutralCulture).Where(cul => cul.LCID != 0x7F); foreach (var culture in cultureInfos) { var region = new RegionInfo(culture.Name); - Regions.Add(culture.Name, region); + _regions.Add(culture.Name, region); } } - return Regions; + return _regions; } /// @@ -52,7 +52,7 @@ public static class Culture _sysCultures.Clear(); break; case CacheType.Region: - Regions.Clear(); + _regions.Clear(); break; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); diff --git a/DotBased/Utilities/Suffix.cs b/DotBased/Utilities/Suffix.cs index 890f93b..60ed314 100755 --- a/DotBased/Utilities/Suffix.cs +++ b/DotBased/Utilities/Suffix.cs @@ -12,30 +12,29 @@ public static class Suffix /// Converts the bytes to the memory suffix. /// /// The bytes to convert - /// How manny decimal places will be placed + /// How manay decimal places will be placed /// The suffixed bytes in the correct format public static string BytesToSizeSuffix(long bytes, int decimalPlaces = 1) { - if (decimalPlaces < 0) decimalPlaces = 1; - - if (bytes == 0) - return $"{0.ToString($"N{decimalPlaces}")} bytes"; - - var negative = bytes < 0; - var absBytes = (ulong)(negative ? -bytes : bytes); - - var mag = (int)Math.Log(absBytes, 1024); - var adjustedSize = absBytes / Math.Pow(1024, mag); - - if (Math.Round(adjustedSize, decimalPlaces) >= 1000 && mag < SizeSuffixes.Length - 1) + if (decimalPlaces < 0) + decimalPlaces = 1; + switch (bytes) { - mag++; - adjustedSize /= 1024; + case < 0: + return "-" + BytesToSizeSuffix(-bytes, decimalPlaces); + case 0: + return string.Format("{0:n" + decimalPlaces + "} bytes", 0); } - var format = $"N{decimalPlaces}"; - var result = $"{adjustedSize.ToString(format)} {SizeSuffixes[mag]}"; + int mag = (int)Math.Log(bytes, 1024); - return negative ? "-" + result : result; + decimal adjustedSize = (decimal)bytes / (1L << (mag * 10)); + + if (Math.Round(adjustedSize, decimalPlaces) >= 1000) + { + mag += 1; + adjustedSize /= 1024; + } + return string.Format("{0:n" + decimalPlaces + "} {1}", adjustedSize, SizeSuffixes[mag]); } } \ No newline at end of file