diff --git a/.idea/.idea.HopFrame/.idea/dataSources.xml b/.idea/.idea.HopFrame/.idea/dataSources.xml index 56f170e..ded00e9 100644 --- a/.idea/.idea.HopFrame/.idea/dataSources.xml +++ b/.idea/.idea.HopFrame/.idea/dataSources.xml @@ -5,7 +5,7 @@ sqlite.xerial true org.sqlite.JDBC - jdbc:sqlite:$PROJECT_DIR$/RestApiTest/bin/Debug/net8.0/test.db + jdbc:sqlite:$PROJECT_DIR$/test/RestApiTest/bin/Debug/net8.0/test.db diff --git a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs index 29feb38..618a437 100644 --- a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs @@ -27,10 +27,11 @@ public static class ServiceCollectionExtensions { /// The service provider to add the services to /// The data source for all HopFrame entities public static void AddHopFrameNoEndpoints(this IServiceCollection services) where TDbContext : HopDbContextBase { + services.AddHopFrameRepositories(); services.TryAddSingleton(); - services.AddScoped>(); + services.AddScoped(); - services.AddHopFrameAuthentication(); + services.AddHopFrameAuthentication(); } } diff --git a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs index 405c2cf..e792add 100644 --- a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs +++ b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs @@ -1,16 +1,14 @@ using HopFrame.Api.Models; -using HopFrame.Database; -using HopFrame.Database.Models.Entries; +using HopFrame.Database.Models; +using HopFrame.Database.Repositories; using HopFrame.Security.Authentication; using HopFrame.Security.Claims; using HopFrame.Security.Models; -using HopFrame.Security.Services; using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; namespace HopFrame.Api.Logic.Implementation; -public class AuthLogic(TDbContext context, IUserService users, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic where TDbContext : HopDbContextBase { +public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic { public async Task>> Login(UserLogin login) { var user = await users.GetUserByEmail(login.Email); @@ -21,34 +19,21 @@ public class AuthLogic(TDbContext context, IUserService users, IToke if (!await users.CheckUserPassword(user, login.Password)) return LogicResult>.Forbidden("The provided password is not correct"); - var refreshToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.RefreshTokenType, - UserId = user.Id.ToString() - }; - var accessToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.AccessTokenType, - UserId = user.Id.ToString() - }; + var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); + var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.RefreshTokenTime, HttpOnly = true, Secure = true }); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = true, Secure = true }); - await context.Tokens.AddRangeAsync(refreshToken, accessToken); - await context.SaveChangesAsync(); - - return LogicResult>.Ok(accessToken.Token); + return LogicResult>.Ok(accessToken.Content.ToString()); } public async Task>> Register(UserRegister register) { @@ -59,36 +44,27 @@ public class AuthLogic(TDbContext context, IUserService users, IToke if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email)) return LogicResult>.Conflict("Username or Email is already registered"); - var user = await users.AddUser(register); + var user = await users.AddUser(new User { + Username = register.Username, + Email = register.Email, + Password = register.Password + }); - var refreshToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.RefreshTokenType, - UserId = user.Id.ToString() - }; - var accessToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.AccessTokenType, - UserId = user.Id.ToString() - }; + var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); + var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - await context.Tokens.AddRangeAsync(refreshToken, accessToken); - await context.SaveChangesAsync(); - - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.RefreshTokenTime, HttpOnly = true, Secure = true }); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = false, Secure = true }); - return LogicResult>.Ok(accessToken.Token); + return LogicResult>.Ok(accessToken.Content.ToString()); } public async Task>> Authenticate() { @@ -97,31 +73,26 @@ public class AuthLogic(TDbContext context, IUserService users, IToke if (string.IsNullOrEmpty(refreshToken)) return LogicResult>.Conflict("Refresh token not provided"); - var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType); - + var token = await tokens.GetToken(refreshToken); + + if (token.Type != Token.RefreshTokenType) + return LogicResult>.BadRequest("The provided token is not a refresh token"); + if (token is null) return LogicResult>.NotFound("Refresh token not valid"); - if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) + if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return LogicResult>.Conflict("Refresh token is expired"); - var accessToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.AccessTokenType, - UserId = token.UserId - }; - - await context.Tokens.AddAsync(accessToken); - await context.SaveChangesAsync(); + var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = false, Secure = true }); - return LogicResult>.Ok(accessToken.Token); + return LogicResult>.Ok(accessToken.Content.ToString()); } public async Task Logout() { @@ -131,17 +102,7 @@ public class AuthLogic(TDbContext context, IUserService users, IToke if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken)) return LogicResult.Conflict("access or refresh token not provided"); - var tokenEntries = await context.Tokens.Where(token => - (token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) || - (token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType)) - .ToArrayAsync(); - - if (tokenEntries.Length != 2) - return LogicResult.NotFound("One or more of the provided tokens was not found"); - - context.Tokens.Remove(tokenEntries[0]); - context.Tokens.Remove(tokenEntries[1]); - await context.SaveChangesAsync(); + await tokens.DeleteUserTokens(tokenContext.User); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType); diff --git a/src/HopFrame.Security/EncryptionManager.cs b/src/HopFrame.Database/EncryptionManager.cs similarity index 96% rename from src/HopFrame.Security/EncryptionManager.cs rename to src/HopFrame.Database/EncryptionManager.cs index 8f5037b..32bb2d5 100644 --- a/src/HopFrame.Security/EncryptionManager.cs +++ b/src/HopFrame.Database/EncryptionManager.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Cryptography.KeyDerivation; -namespace HopFrame.Security; +namespace HopFrame.Database; public static class EncryptionManager { diff --git a/src/HopFrame.Database/HopDbContextBase.cs b/src/HopFrame.Database/HopDbContextBase.cs index 2ae1fe6..21342ea 100644 --- a/src/HopFrame.Database/HopDbContextBase.cs +++ b/src/HopFrame.Database/HopDbContextBase.cs @@ -1,4 +1,4 @@ -using HopFrame.Database.Models.Entries; +using HopFrame.Database.Models; using Microsoft.EntityFrameworkCore; namespace HopFrame.Database; @@ -8,25 +8,27 @@ namespace HopFrame.Database; /// public abstract class HopDbContextBase : DbContext { - public virtual DbSet Users { get; set; } - public virtual DbSet Permissions { get; set; } - public virtual DbSet Tokens { get; set; } - public virtual DbSet Groups { get; set; } + public virtual DbSet Users { get; set; } + public virtual DbSet Permissions { get; set; } + public virtual DbSet Tokens { get; set; } + public virtual DbSet Groups { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - } + modelBuilder.Entity() + .HasMany(u => u.Tokens) + .WithOne(t => t.Owner) + .OnDelete(DeleteBehavior.Cascade); - /// - /// Gets executed when a user is deleted through the IUserService from the - /// HopFrame.Security package. You can override this method to also delete - /// related user specific entries in the database - /// - /// - public virtual void OnUserDelete(UserEntry user) {} + modelBuilder.Entity() + .HasMany(u => u.Permissions) + .WithOne(p => p.User) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasMany(g => g.Permissions) + .WithOne(p => p.Group) + .OnDelete(DeleteBehavior.Cascade); + } } \ No newline at end of file diff --git a/src/HopFrame.Database/HopFrame.Database.csproj b/src/HopFrame.Database/HopFrame.Database.csproj index d88b757..f2bcba7 100644 --- a/src/HopFrame.Database/HopFrame.Database.csproj +++ b/src/HopFrame.Database/HopFrame.Database.csproj @@ -14,6 +14,7 @@ + diff --git a/src/HopFrame.Database/Models/Entries/GroupEntry.cs b/src/HopFrame.Database/Models/Entries/GroupEntry.cs deleted file mode 100644 index 830d466..0000000 --- a/src/HopFrame.Database/Models/Entries/GroupEntry.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; - -namespace HopFrame.Database.Models.Entries; - -public class GroupEntry { - [Key, Required, MaxLength(50)] - public string Name { get; set; } - - [Required, DefaultValue(false)] - public bool Default { get; set; } - - [MaxLength(500)] - public string Description { get; set; } - - [Required] - public DateTime CreatedAt { get; set; } -} \ No newline at end of file diff --git a/src/HopFrame.Database/Models/Entries/PermissionEntry.cs b/src/HopFrame.Database/Models/Entries/PermissionEntry.cs deleted file mode 100644 index 2f8bdae..0000000 --- a/src/HopFrame.Database/Models/Entries/PermissionEntry.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace HopFrame.Database.Models.Entries; - -public sealed class PermissionEntry { - [Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long RecordId { get; set; } - - [Required, MaxLength(255)] - public string PermissionText { get; set; } - - [Required, MinLength(36), MaxLength(36)] - public string UserId { get; set; } - - [Required] - public DateTime GrantedAt { get; set; } -} \ No newline at end of file diff --git a/src/HopFrame.Database/Models/Entries/UserEntry.cs b/src/HopFrame.Database/Models/Entries/UserEntry.cs deleted file mode 100644 index 2bc1a12..0000000 --- a/src/HopFrame.Database/Models/Entries/UserEntry.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace HopFrame.Database.Models.Entries; - -public class UserEntry { - [Key, Required, MinLength(36), MaxLength(36)] - public string Id { get; set; } - - [MaxLength(50)] - public string Username { get; set; } - - [Required, MaxLength(50), EmailAddress] - public string Email { get; set; } - - [Required, MinLength(8), MaxLength(255)] - public string Password { get; set; } - - [Required] - public DateTime CreatedAt { get; set; } -} \ No newline at end of file diff --git a/src/HopFrame.Database/Models/ModelExtensions.cs b/src/HopFrame.Database/Models/ModelExtensions.cs deleted file mode 100644 index 4600afd..0000000 --- a/src/HopFrame.Database/Models/ModelExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using HopFrame.Database.Models.Entries; - -namespace HopFrame.Database.Models; - -public static class ModelExtensions { - - /// - /// Converts the database model to a friendly user model - /// - /// the database model - /// the data source for the permissions and users - /// - public static User ToUserModel(this UserEntry entry, HopDbContextBase contextBase) { - var user = new User { - Id = Guid.Parse(entry.Id), - Username = entry.Username, - Email = entry.Email, - CreatedAt = entry.CreatedAt - }; - - user.Permissions = contextBase.Permissions - .Where(perm => perm.UserId == entry.Id) - .Select(perm => perm.ToPermissionModel()) - .ToList(); - - return user; - } - - public static Permission ToPermissionModel(this PermissionEntry entry) { - Guid.TryParse(entry.UserId, out var userId); - - return new Permission { - Owner = userId, - PermissionName = entry.PermissionText, - GrantedAt = entry.GrantedAt, - Id = entry.RecordId - }; - } - - public static PermissionGroup ToPermissionGroup(this GroupEntry entry, HopDbContextBase contextBase) { - var group = new PermissionGroup { - Name = entry.Name, - IsDefaultGroup = entry.Default, - Description = entry.Description, - CreatedAt = entry.CreatedAt - }; - - group.Permissions = contextBase.Permissions - .Where(perm => perm.UserId == group.Name) - .Select(perm => perm.ToPermissionModel()) - .ToList(); - - return group; - } - -} \ No newline at end of file diff --git a/src/HopFrame.Database/Models/Permission.cs b/src/HopFrame.Database/Models/Permission.cs index e6fbe14..db111ba 100644 --- a/src/HopFrame.Database/Models/Permission.cs +++ b/src/HopFrame.Database/Models/Permission.cs @@ -1,10 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + namespace HopFrame.Database.Models; -public sealed class Permission { +public class Permission { + + [Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; init; } + + [Required, MaxLength(255)] public string PermissionName { get; set; } - public Guid Owner { get; set; } + + [Required] public DateTime GrantedAt { get; set; } + + [ForeignKey("UserId"), JsonIgnore] + public virtual User User { get; set; } + + [ForeignKey("GroupName"), JsonIgnore] + public virtual PermissionGroup Group { get; set; } + } -public interface IPermissionOwner {} +public interface IPermissionOwner; diff --git a/src/HopFrame.Database/Models/PermissionGroup.cs b/src/HopFrame.Database/Models/PermissionGroup.cs index 3472e39..7a70ebd 100644 --- a/src/HopFrame.Database/Models/PermissionGroup.cs +++ b/src/HopFrame.Database/Models/PermissionGroup.cs @@ -1,9 +1,22 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + namespace HopFrame.Database.Models; public class PermissionGroup : IPermissionOwner { + + [Key, Required, MaxLength(50)] public string Name { get; init; } + + [Required, DefaultValue(false)] public bool IsDefaultGroup { get; set; } + + [MaxLength(500)] public string Description { get; set; } + + [Required] public DateTime CreatedAt { get; set; } - public IList Permissions { get; set; } + + public virtual IList Permissions { get; set; } + } \ No newline at end of file diff --git a/src/HopFrame.Database/Models/Entries/TokenEntry.cs b/src/HopFrame.Database/Models/Token.cs similarity index 62% rename from src/HopFrame.Database/Models/Entries/TokenEntry.cs rename to src/HopFrame.Database/Models/Token.cs index d33b307..a42d367 100644 --- a/src/HopFrame.Database/Models/Entries/TokenEntry.cs +++ b/src/HopFrame.Database/Models/Token.cs @@ -1,8 +1,10 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; -namespace HopFrame.Database.Models.Entries; +namespace HopFrame.Database.Models; -public class TokenEntry { +public class Token { public const int RefreshTokenType = 0; public const int AccessTokenType = 1; @@ -15,11 +17,11 @@ public class TokenEntry { public int Type { get; set; } [Key, Required, MinLength(36), MaxLength(36)] - public string Token { get; set; } - - [Required, MinLength(36), MaxLength(36)] - public string UserId { get; set; } + public Guid Content { get; set; } [Required] public DateTime CreatedAt { get; set; } + + [ForeignKey("UserId"), JsonIgnore] + public virtual User Owner { get; set; } } \ No newline at end of file diff --git a/src/HopFrame.Database/Models/User.cs b/src/HopFrame.Database/Models/User.cs index e97d720..971d899 100644 --- a/src/HopFrame.Database/Models/User.cs +++ b/src/HopFrame.Database/Models/User.cs @@ -1,9 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + namespace HopFrame.Database.Models; -public sealed class User : IPermissionOwner { +public class User : IPermissionOwner { + + [Key, Required, MinLength(36), MaxLength(36)] public Guid Id { get; init; } + + [MaxLength(50)] public string Username { get; set; } + + [Required, MaxLength(50), EmailAddress] public string Email { get; set; } + + [Required, MinLength(8), MaxLength(255), JsonIgnore] + public string Password { get; set; } + + [Required] public DateTime CreatedAt { get; set; } - public IList Permissions { get; set; } + + public virtual IList Permissions { get; set; } + + [JsonIgnore] + public virtual IList Tokens { get; set; } + } \ No newline at end of file diff --git a/src/HopFrame.Security/Authorization/PermissionValidator.cs b/src/HopFrame.Database/PermissionValidator.cs similarity index 94% rename from src/HopFrame.Security/Authorization/PermissionValidator.cs rename to src/HopFrame.Database/PermissionValidator.cs index d3a844b..de19dfc 100644 --- a/src/HopFrame.Security/Authorization/PermissionValidator.cs +++ b/src/HopFrame.Database/PermissionValidator.cs @@ -1,4 +1,4 @@ -namespace HopFrame.Security.Authorization; +namespace HopFrame.Database; public static class PermissionValidator { diff --git a/src/HopFrame.Database/Repositories/IGroupRepository.cs b/src/HopFrame.Database/Repositories/IGroupRepository.cs new file mode 100644 index 0000000..259d7c5 --- /dev/null +++ b/src/HopFrame.Database/Repositories/IGroupRepository.cs @@ -0,0 +1,21 @@ +using HopFrame.Database.Models; + +namespace HopFrame.Database.Repositories; + +public interface IGroupRepository { + Task> GetPermissionGroups(); + + Task> GetDefaultGroups(); + + Task> GetUserGroups(User user); + + Task GetPermissionGroup(string name); + + Task EditPermissionGroup(PermissionGroup group); + + Task CreatePermissionGroup(PermissionGroup group); + + Task DeletePermissionGroup(PermissionGroup group); + + internal Task> GetFullGroupPermissions(string group); +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/IPermissionRepository.cs b/src/HopFrame.Database/Repositories/IPermissionRepository.cs new file mode 100644 index 0000000..5971e34 --- /dev/null +++ b/src/HopFrame.Database/Repositories/IPermissionRepository.cs @@ -0,0 +1,23 @@ +using HopFrame.Database.Models; + +namespace HopFrame.Database.Repositories; + +public interface IPermissionRepository { + Task HasPermission(IPermissionOwner owner, params string[] permissions); + + /// + /// permission system:
+ /// - "*" -> all rights
+ /// - "group.[name]" -> group member
+ /// - "[namespace].[name]" -> single permission
+ /// - "[namespace].*" -> all permissions in the namespace + ///
+ /// + /// + /// + Task AddPermission(IPermissionOwner owner, string permission); + + Task RemovePermission(IPermissionOwner owner, string permission); + + public Task> GetFullPermissions(IPermissionOwner owner); +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/ITokenRepository.cs b/src/HopFrame.Database/Repositories/ITokenRepository.cs new file mode 100644 index 0000000..19b38ac --- /dev/null +++ b/src/HopFrame.Database/Repositories/ITokenRepository.cs @@ -0,0 +1,9 @@ +using HopFrame.Database.Models; + +namespace HopFrame.Database.Repositories; + +public interface ITokenRepository { + public Task GetToken(string content); + public Task CreateToken(int type, User owner); + public Task DeleteUserTokens(User owner); +} \ No newline at end of file diff --git a/src/HopFrame.Security/Services/IUserService.cs b/src/HopFrame.Database/Repositories/IUserRepository.cs similarity index 53% rename from src/HopFrame.Security/Services/IUserService.cs rename to src/HopFrame.Database/Repositories/IUserRepository.cs index 5109dea..e847d61 100644 --- a/src/HopFrame.Security/Services/IUserService.cs +++ b/src/HopFrame.Database/Repositories/IUserRepository.cs @@ -1,9 +1,8 @@ using HopFrame.Database.Models; -using HopFrame.Security.Models; -namespace HopFrame.Security.Services; +namespace HopFrame.Database.Repositories; -public interface IUserService { +public interface IUserRepository { Task> GetUsers(); Task GetUser(Guid userId); @@ -12,13 +11,8 @@ public interface IUserService { Task GetUserByUsername(string username); - Task AddUser(UserRegister user); - - /// - /// IMPORTANT:
- /// This function does not add or remove any permissions to the user. - /// For that please use - ///
+ Task AddUser(User user); + Task UpdateUser(User user); Task DeleteUser(User user); diff --git a/src/HopFrame.Database/Repositories/Implementation/GroupRepository.cs b/src/HopFrame.Database/Repositories/Implementation/GroupRepository.cs new file mode 100644 index 0000000..547e193 --- /dev/null +++ b/src/HopFrame.Database/Repositories/Implementation/GroupRepository.cs @@ -0,0 +1,79 @@ +using HopFrame.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Database.Repositories.Implementation; + +internal sealed class GroupRepository(TDbContext context) : IGroupRepository where TDbContext : HopDbContextBase { + public async Task> GetPermissionGroups() { + return await context.Groups + .Include(g => g.Permissions) + .ToListAsync(); + } + + public async Task> GetDefaultGroups() { + return await context.Groups + .Include(g => g.Permissions) + .Where(g => g.IsDefaultGroup) + .ToListAsync(); + } + + public Task> GetUserGroups(User user) { + return Task.FromResult((IList) context.Groups + .Include(g => g.Permissions) + .AsEnumerable() + .Where(g => user.Permissions.Any(p => p.PermissionName == g.Name)) + .ToList()); + } + + public async Task GetPermissionGroup(string name) { + return await context.Groups + .Include(g => g.Permissions) + .Where(g => g.Name == name) + .SingleOrDefaultAsync(); + } + + public async Task EditPermissionGroup(PermissionGroup group) { + var orig = await context.Groups.SingleOrDefaultAsync(g => g.Name == group.Name); + + if (orig is null) return; + + var entity = context.Groups.Update(orig); + + entity.Entity.IsDefaultGroup = group.IsDefaultGroup; + entity.Entity.Description = group.Description; + entity.Entity.Permissions = group.Permissions; + + await context.SaveChangesAsync(); + } + + public async Task CreatePermissionGroup(PermissionGroup group) { + group.CreatedAt = DateTime.Now; + await context.Groups.AddAsync(group); + await context.SaveChangesAsync(); + return group; + } + + public async Task DeletePermissionGroup(PermissionGroup group) { + context.Groups.Remove(group); + await context.SaveChangesAsync(); + } + + public async Task> GetFullGroupPermissions(string group) { + var permissions = await context.Permissions + .Include(p => p.Group) + .Where(p => p.Group != null) + .Where(p => p.Group.Name == group) + .Select(p => p.PermissionName) + .ToListAsync(); + + var groups = permissions + .Where(p => p.StartsWith("group.")) + .ToList(); + + foreach (var subgroup in groups) { + permissions.AddRange(await GetFullGroupPermissions(subgroup)); + } + + return permissions; + } +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs new file mode 100644 index 0000000..45bcfd8 --- /dev/null +++ b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs @@ -0,0 +1,89 @@ +using HopFrame.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Database.Repositories.Implementation; + +internal sealed class PermissionRepository(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase { + public async Task HasPermission(IPermissionOwner owner, params string[] permissions) { + var perms = (await GetFullPermissions(owner)).ToArray(); + + foreach (var permission in permissions) { + if (!PermissionValidator.IncludesPermission(permission, perms)) return false; + } + + return true; + } + + public async Task AddPermission(IPermissionOwner owner, string permission) { + var entry = new Permission { + GrantedAt = DateTime.Now, + PermissionName = permission + }; + + if (owner is User user) { + entry.User = user; + }else if (owner is PermissionGroup group) { + entry.Group = group; + } + + await context.Permissions.AddAsync(entry); + await context.SaveChangesAsync(); + return entry; + } + + public async Task RemovePermission(IPermissionOwner owner, string permission) { + Permission entry = null; + + if (owner is User user) { + entry = await context.Permissions + .Include(p => p.User) + .Where(p => p.User != null) + .Where(p => p.User.Id == user.Id) + .Where(p => p.PermissionName == permission) + .SingleOrDefaultAsync(); + }else if (owner is PermissionGroup group) { + entry = await context.Permissions + .Include(p => p.Group) + .Where(p => p.Group != null) + .Where(p =>p.Group.Name == group.Name) + .Where(p => p.PermissionName == permission) + .SingleOrDefaultAsync(); + } + + if (entry is not null) { + context.Permissions.Remove(entry); + await context.SaveChangesAsync(); + } + } + + public async Task> GetFullPermissions(IPermissionOwner owner) { + var permissions = new List(); + + if (owner is User user) { + var perms = await context.Permissions + .Include(p => p.User) + .Where(p => p.User != null) + .Where(p => p.User.Id == user.Id) + .ToListAsync(); + + permissions.AddRange(perms.Select(p => p.PermissionName)); + }else if (owner is PermissionGroup group) { + var perms = await context.Permissions + .Include(p => p.Group) + .Where(p => p.Group != null) + .Where(p =>p.Group.Name == group.Name) + .ToListAsync(); + + permissions.AddRange(perms.Select(p => p.PermissionName)); + } + + var groups = permissions + .Where(p => p.StartsWith("group.")) + .ToList(); + foreach (var group in groups) { + permissions.AddRange(await groupRepository.GetFullGroupPermissions(group)); + } + + return permissions; + } +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs new file mode 100644 index 0000000..70f727a --- /dev/null +++ b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs @@ -0,0 +1,41 @@ +using HopFrame.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Database.Repositories.Implementation; + +internal sealed class TokenRepository(TDbContext context) : ITokenRepository where TDbContext : HopDbContextBase { + + public async Task GetToken(string content) { + var success = Guid.TryParse(content, out Guid guid); + if (!success) return null; + + return await context.Tokens + .Include(t => t.Owner) + .Where(t => t.Content == guid) + .SingleOrDefaultAsync(); + } + + public async Task CreateToken(int type, User owner) { + var token = new Token { + CreatedAt = DateTime.Now, + Content = Guid.NewGuid(), + Type = type, + Owner = owner + }; + + await context.Tokens.AddAsync(token); + await context.SaveChangesAsync(); + + return token; + } + + public async Task DeleteUserTokens(User owner) { + var tokens = await context.Tokens + .Include(t => t.Owner) + .Where(t => t.Owner.Id == owner.Id) + .ToListAsync(); + + context.Tokens.RemoveRange(tokens); + await context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/Implementation/UserRepository.cs b/src/HopFrame.Database/Repositories/Implementation/UserRepository.cs new file mode 100644 index 0000000..c642466 --- /dev/null +++ b/src/HopFrame.Database/Repositories/Implementation/UserRepository.cs @@ -0,0 +1,113 @@ +using System.Globalization; +using System.Text; +using HopFrame.Database.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; + +namespace HopFrame.Database.Repositories.Implementation; + +internal sealed class UserRepository(TDbContext context, IGroupRepository groupRepository) : IUserRepository where TDbContext : HopDbContextBase { + + private IIncludableQueryable> IncludeReferences() { + return context.Users + .Include(u => u.Permissions) + .Include(u => u.Tokens); + } + + public async Task> GetUsers() { + return await IncludeReferences() + .ToListAsync(); + } + + public async Task GetUser(Guid userId) { + return await IncludeReferences() + .Where(u => u.Id == userId) + .SingleOrDefaultAsync(); + } + + public async Task GetUserByEmail(string email) { + return await IncludeReferences() + .Where(u => u.Email == email) + .SingleOrDefaultAsync(); + } + + public async Task GetUserByUsername(string username) { + return await IncludeReferences() + .Where(u => u.Username == username) + .SingleOrDefaultAsync(); + } + + public async Task AddUser(User user) { + if (await GetUserByEmail(user.Email) is not null) return null; + if (await GetUserByUsername(user.Username) is not null) return null; + + var entry = new User { + Id = Guid.NewGuid(), + Email = user.Email, + Username = user.Username, + CreatedAt = DateTime.Now, + Permissions = user.Permissions ?? new List(), + Tokens = user.Tokens + }; + entry.Password = EncryptionManager.Hash(user.Password, Encoding.Default.GetBytes(entry.CreatedAt.ToString(CultureInfo.InvariantCulture))); + + var defaultGroups = await groupRepository.GetDefaultGroups(); + foreach (var group in defaultGroups) { + entry.Permissions.Add(new Permission { + PermissionName = group.Name, + GrantedAt = DateTime.Now + }); + } + + await context.Users.AddAsync(entry); + await context.SaveChangesAsync(); + return entry; + } + + public async Task UpdateUser(User user) { + var entry = await IncludeReferences() + .SingleOrDefaultAsync(entry => entry.Id == user.Id); + if (entry is null) return; + + entry.Email = user.Email; + entry.Username = user.Username; + entry.Permissions = user.Permissions; + entry.Tokens = user.Tokens; + + await context.SaveChangesAsync(); + } + + public async Task DeleteUser(User user) { + var entry = await context.Users + .SingleOrDefaultAsync(entry => entry.Id == user.Id); + + if (entry is null) return; + + context.Users.Remove(entry); + + await context.SaveChangesAsync(); + } + + public async Task CheckUserPassword(User user, string password) { + var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture))); + + var entry = await context.Users + .Where(entry => entry.Id == user.Id) + .SingleOrDefaultAsync(); + + return entry.Password == hash; + } + + public async Task ChangePassword(User user, string password) { + var entry = await context.Users + .Where(entry => entry.Id == user.Id) + .SingleOrDefaultAsync(); + + if (entry is null) return; + + var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture))); + entry.Password = hash; + await context.SaveChangesAsync(); + } + +} \ No newline at end of file diff --git a/src/HopFrame.Database/ServiceCollectionExtensions.cs b/src/HopFrame.Database/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ab07d5a --- /dev/null +++ b/src/HopFrame.Database/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using HopFrame.Database.Repositories; +using HopFrame.Database.Repositories.Implementation; +using Microsoft.Extensions.DependencyInjection; + +namespace HopFrame.Database; + +public static class ServiceCollectionExtensions { + + public static IServiceCollection AddHopFrameRepositories(this IServiceCollection services) where TDbContext : HopDbContextBase { + services.AddScoped>(); + services.AddScoped>(); + services.AddScoped>(); + services.AddScoped>(); + + return services; + } + +} \ No newline at end of file diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs index 8232d4c..9c65b14 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs @@ -1,10 +1,8 @@ using System.Security.Claims; using System.Text.Encodings.Web; -using HopFrame.Database; +using HopFrame.Database.Repositories; using HopFrame.Security.Claims; -using HopFrame.Security.Services; using Microsoft.AspNetCore.Authentication; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,15 +11,15 @@ using Microsoft.Extensions.Options; namespace HopFrame.Security.Authentication; -public class HopFrameAuthentication( +public class HopFrameAuthentication( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, - TDbContext context, - IPermissionService perms) - : AuthenticationHandler(options, logger, encoder, clock) - where TDbContext : HopDbContextBase { + ITokenRepository tokens, + IUserRepository users, + IPermissionRepository perms) + : AuthenticationHandler(options, logger, encoder, clock) { public const string SchemeName = "HopCore.Authentication"; public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0); @@ -32,21 +30,21 @@ public class HopFrameAuthentication( if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"]; if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided"); - - var tokenEntry = await context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken); + + var tokenEntry = await tokens.GetToken(accessToken); if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist"); if (tokenEntry.CreatedAt + AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired"); - if (!await context.Users.AnyAsync(user => user.Id == tokenEntry.UserId)) + if (tokenEntry.Owner is null) return AuthenticateResult.Fail("The provided Access Token does not match any user"); var claims = new List { new(HopFrameClaimTypes.AccessTokenId, accessToken), - new(HopFrameClaimTypes.UserId, tokenEntry.UserId) + new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString()) }; - var permissions = await perms.GetFullPermissions(tokenEntry.UserId); + var permissions = await perms.GetFullPermissions(tokenEntry.Owner); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); var principal = new ClaimsPrincipal(); diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs index f604c34..cf87810 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs @@ -1,7 +1,4 @@ -using HopFrame.Database; using HopFrame.Security.Claims; -using HopFrame.Security.Services; -using HopFrame.Security.Services.Implementation; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -17,13 +14,11 @@ public static class HopFrameAuthenticationExtensions { /// The service provider to add the services to /// The database object that saves all entities that are important for the security api /// - public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) where TDbContext : HopDbContextBase { + public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) { service.TryAddSingleton(); - service.AddScoped>(); - service.AddScoped>(); - service.AddScoped>(); + service.AddScoped(); - service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme>(HopFrameAuthentication.SchemeName, _ => {}); + service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme(HopFrameAuthentication.SchemeName, _ => {}); service.AddAuthorization(); return service; diff --git a/src/HopFrame.Security/Authorization/AuthorizedFilter.cs b/src/HopFrame.Security/Authorization/AuthorizedFilter.cs index 13f5932..f78cdc0 100644 --- a/src/HopFrame.Security/Authorization/AuthorizedFilter.cs +++ b/src/HopFrame.Security/Authorization/AuthorizedFilter.cs @@ -1,3 +1,4 @@ +using HopFrame.Database; using HopFrame.Security.Claims; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Authorization; diff --git a/src/HopFrame.Security/Claims/ITokenContext.cs b/src/HopFrame.Security/Claims/ITokenContext.cs index 3b4f5e9..6b5a590 100644 --- a/src/HopFrame.Security/Claims/ITokenContext.cs +++ b/src/HopFrame.Security/Claims/ITokenContext.cs @@ -20,5 +20,5 @@ public interface ITokenContext { /// /// The access token the user provided /// - Guid AccessToken { get; } + Token AccessToken { get; } } \ No newline at end of file diff --git a/src/HopFrame.Security/Claims/TokenContextImplementor.cs b/src/HopFrame.Security/Claims/TokenContextImplementor.cs index dbdae9e..dd50a08 100644 --- a/src/HopFrame.Security/Claims/TokenContextImplementor.cs +++ b/src/HopFrame.Security/Claims/TokenContextImplementor.cs @@ -1,15 +1,13 @@ -using HopFrame.Database; using HopFrame.Database.Models; +using HopFrame.Database.Repositories; using Microsoft.AspNetCore.Http; namespace HopFrame.Security.Claims; -internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, TDbContext context) : ITokenContext where TDbContext : HopDbContextBase { +internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens) : ITokenContext { public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId()); - public User User => context.Users - .SingleOrDefault(user => user.Id == accessor.HttpContext.User.GetUserId())? - .ToUserModel(context); - - public Guid AccessToken => Guid.Parse(accessor.HttpContext?.User.GetAccessTokenId() ?? Guid.Empty.ToString()); + public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult(); + + public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult(); } \ No newline at end of file diff --git a/src/HopFrame.Security/Services/IPermissionService.cs b/src/HopFrame.Security/Services/IPermissionService.cs deleted file mode 100644 index 38a9000..0000000 --- a/src/HopFrame.Security/Services/IPermissionService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using HopFrame.Database.Models; - -namespace HopFrame.Security.Services; - -/// -/// permission system:
-/// - "*" -> all rights
-/// - "group.[name]" -> group member
-/// - "[namespace].[name]" -> single permission
-/// - "[namespace].*" -> all permissions in the namespace -///
-public interface IPermissionService { - - Task HasPermission(string permission, Guid user); - - Task> GetPermissionGroups(); - - Task GetPermissionGroup(string name); - - Task EditPermissionGroup(PermissionGroup group); - - Task> GetUserPermissionGroups(User user); - - Task RemoveGroupFromUser(User user, PermissionGroup group); - - Task CreatePermissionGroup(string name, bool isDefault = false, string description = null); - - Task DeletePermissionGroup(PermissionGroup group); - - Task GetPermission(string name, IPermissionOwner owner); - - /// - /// permission system:
- /// - "*" -> all rights
- /// - "group.[name]" -> group member
- /// - "[namespace].[name]" -> single permission
- /// - "[namespace].*" -> all permissions in the namespace - ///
- /// - /// - /// - Task AddPermission(IPermissionOwner owner, string permission); - - Task RemovePermission(Permission permission); - - Task GetFullPermissions(string user); - -} \ No newline at end of file diff --git a/src/HopFrame.Security/Services/Implementation/PermissionService.cs b/src/HopFrame.Security/Services/Implementation/PermissionService.cs deleted file mode 100644 index ac0e156..0000000 --- a/src/HopFrame.Security/Services/Implementation/PermissionService.cs +++ /dev/null @@ -1,178 +0,0 @@ -using HopFrame.Database; -using HopFrame.Database.Models; -using HopFrame.Database.Models.Entries; -using HopFrame.Security.Authorization; -using HopFrame.Security.Claims; -using Microsoft.EntityFrameworkCore; - -namespace HopFrame.Security.Services.Implementation; - -internal sealed class PermissionService(TDbContext context, ITokenContext current) : IPermissionService where TDbContext : HopDbContextBase { - public async Task HasPermission(string permission) { - return await HasPermission(permission, current.User.Id); - } - - public async Task HasPermissions(params string[] permissions) { - var user = current.User.Id.ToString(); - var perms = await GetFullPermissions(user); - - foreach (var permission in permissions) { - if (!PermissionValidator.IncludesPermission(permission, perms)) return false; - } - - return true; - } - - public async Task HasAnyPermission(params string[] permissions) { - var user = current.User.Id.ToString(); - var perms = await GetFullPermissions(user); - - foreach (var permission in permissions) { - if (PermissionValidator.IncludesPermission(permission, perms)) return true; - } - - return false; - } - - public async Task HasPermission(string permission, Guid user) { - var permissions = await GetFullPermissions(user.ToString()); - - return PermissionValidator.IncludesPermission(permission, permissions); - } - - public async Task> GetPermissionGroups() { - return await context.Groups - .Select(group => group.ToPermissionGroup(context)) - .ToListAsync(); - } - - public Task GetPermissionGroup(string name) { - return context.Groups - .Where(group => group.Name == name) - .Select(group => group.ToPermissionGroup(context)) - .SingleOrDefaultAsync(); - } - - public async Task EditPermissionGroup(PermissionGroup group) { - var orig = await context.Groups.SingleOrDefaultAsync(g => g.Name == group.Name); - - if (orig is null) return; - - var entity = context.Groups.Update(orig); - - entity.Entity.Default = group.IsDefaultGroup; - entity.Entity.Description = group.Description; - - await context.SaveChangesAsync(); - } - - public async Task> GetUserPermissionGroups(User user) { - var groups = await context.Groups.ToListAsync(); - var perms = await GetFullPermissions(user.Id.ToString()); - - return groups - .Where(group => perms.Contains(group.Name)) - .Select(group => group.ToPermissionGroup(context)) - .ToList(); - } - - public async Task RemoveGroupFromUser(User user, PermissionGroup group) { - var entry = await context.Permissions - .Where(perm => perm.PermissionText == group.Name && perm.UserId == user.Id.ToString()) - .SingleOrDefaultAsync(); - - if (entry is null) return; - - context.Permissions.Remove(entry); - await context.SaveChangesAsync(); - } - - public async Task CreatePermissionGroup(string name, bool isDefault = false, string description = null) { - var group = new GroupEntry { - Name = name, - Description = description, - Default = isDefault, - CreatedAt = DateTime.Now - }; - - await context.Groups.AddAsync(group); - - if (isDefault) { - var users = await context.Users.ToListAsync(); - - foreach (var user in users) { - await context.Permissions.AddAsync(new PermissionEntry { - GrantedAt = DateTime.Now, - PermissionText = group.Name, - UserId = user.Id - }); - } - } - - await context.SaveChangesAsync(); - - return group.ToPermissionGroup(context); - } - - public async Task DeletePermissionGroup(PermissionGroup group) { - var entry = await context.Groups.SingleOrDefaultAsync(entry => entry.Name == group.Name); - context.Groups.Remove(entry); - - var permissions = await context.Permissions - .Where(perm => perm.UserId == group.Name || perm.PermissionText == group.Name) - .ToListAsync(); - - if (permissions.Count > 0) { - context.Permissions.RemoveRange(permissions); - } - - await context.SaveChangesAsync(); - } - - public async Task GetPermission(string name, IPermissionOwner owner) { - var ownerId = (owner is User user) ? user.Id.ToString() : ((PermissionGroup)owner).Name; - - return await context.Permissions - .Where(perm => perm.PermissionText == name && perm.UserId == ownerId) - .Select(perm => perm.ToPermissionModel()) - .SingleOrDefaultAsync(); - } - - public async Task AddPermission(IPermissionOwner owner, string permission) { - var userId = owner is User user ? user.Id.ToString() : (owner as PermissionGroup)?.Name; - - await context.Permissions.AddAsync(new PermissionEntry { - UserId = userId, - PermissionText = permission, - GrantedAt = DateTime.Now - }); - await context.SaveChangesAsync(); - } - - public async Task RemovePermission(Permission permission) { - var entry = await context.Permissions.SingleOrDefaultAsync(entry => entry.RecordId == permission.Id); - context.Permissions.Remove(entry); - await context.SaveChangesAsync(); - } - - public async Task GetFullPermissions(string user) { - var permissions = await context.Permissions - .Where(perm => perm.UserId == user) - .Select(perm => perm.PermissionText) - .ToListAsync(); - - var groups = permissions - .Where(perm => perm.StartsWith("group.")) - .ToList(); - - var groupPerms = new List(); - foreach (var group in groups) { - var perms = await GetFullPermissions(group); - groupPerms.AddRange(perms); - } - - permissions.AddRange(groupPerms); - - return permissions.ToArray(); - } -} \ No newline at end of file diff --git a/src/HopFrame.Security/Services/Implementation/UserService.cs b/src/HopFrame.Security/Services/Implementation/UserService.cs deleted file mode 100644 index 0e19b58..0000000 --- a/src/HopFrame.Security/Services/Implementation/UserService.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System.Globalization; -using System.Text; -using HopFrame.Database; -using HopFrame.Database.Models; -using HopFrame.Database.Models.Entries; -using HopFrame.Security.Models; -using Microsoft.EntityFrameworkCore; - -namespace HopFrame.Security.Services.Implementation; - -internal sealed class UserService(TDbContext context) : IUserService where TDbContext : HopDbContextBase { - public async Task> GetUsers() { - return await context.Users - .Select(user => user.ToUserModel(context)) - .ToListAsync(); - } - - public Task GetUser(Guid userId) { - var id = userId.ToString(); - - return context.Users - .Where(user => user.Id == id) - .Select(user => user.ToUserModel(context)) - .SingleOrDefaultAsync(); - } - - public Task GetUserByEmail(string email) { - return context.Users - .Where(user => user.Email == email) - .Select(user => user.ToUserModel(context)) - .SingleOrDefaultAsync(); - } - - public Task GetUserByUsername(string username) { - return context.Users - .Where(user => user.Username == username) - .Select(user => user.ToUserModel(context)) - .SingleOrDefaultAsync(); - } - - public async Task AddUser(UserRegister user) { - if (await GetUserByEmail(user.Email) is not null) return null; - if (await GetUserByUsername(user.Username) is not null) return null; - - var entry = new UserEntry { - Id = Guid.NewGuid().ToString(), - Email = user.Email, - Username = user.Username, - CreatedAt = DateTime.Now - }; - entry.Password = EncryptionManager.Hash(user.Password, Encoding.Default.GetBytes(entry.CreatedAt.ToString(CultureInfo.InvariantCulture))); - - await context.Users.AddAsync(entry); - - var defaultGroups = await context.Groups - .Where(group => group.Default) - .Select(group => "group." + group.Name) - .ToListAsync(); - - await context.Permissions.AddRangeAsync(defaultGroups.Select(group => new PermissionEntry { - GrantedAt = DateTime.Now, - PermissionText = group, - UserId = entry.Id - })); - - await context.SaveChangesAsync(); - return entry.ToUserModel(context); - } - - public async Task UpdateUser(User user) { - var id = user.Id.ToString(); - var entry = await context.Users - .SingleOrDefaultAsync(entry => entry.Id == id); - if (entry is null) return; - - entry.Email = user.Email; - entry.Username = user.Username; - - await context.SaveChangesAsync(); - } - - public async Task DeleteUser(User user) { - var id = user.Id.ToString(); - var entry = await context.Users - .SingleOrDefaultAsync(entry => entry.Id == id); - - if (entry is null) return; - - context.Users.Remove(entry); - - var userTokens = await context.Tokens - .Where(token => token.UserId == id) - .ToArrayAsync(); - context.Tokens.RemoveRange(userTokens); - - var userPermissions = await context.Permissions - .Where(perm => perm.UserId == id) - .ToArrayAsync(); - context.Permissions.RemoveRange(userPermissions); - - context.OnUserDelete(entry); - - await context.SaveChangesAsync(); - } - - public async Task CheckUserPassword(User user, string password) { - var id = user.Id.ToString(); - var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture))); - - var entry = await context.Users - .Where(entry => entry.Id == id) - .SingleOrDefaultAsync(); - - return entry.Password == hash; - } - - public async Task ChangePassword(User user, string password) { - var entry = await context.Users - .Where(entry => entry.Id == user.Id.ToString()) - .SingleOrDefaultAsync(); - - if (entry is null) return; - - var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture))); - entry.Password = hash; - await context.SaveChangesAsync(); - } -} \ No newline at end of file diff --git a/src/HopFrame.Web/AuthMiddleware.cs b/src/HopFrame.Web/AuthMiddleware.cs index bc0c0c5..509ad83 100644 --- a/src/HopFrame.Web/AuthMiddleware.cs +++ b/src/HopFrame.Web/AuthMiddleware.cs @@ -1,14 +1,13 @@ using System.Security.Claims; -using HopFrame.Database; +using HopFrame.Database.Repositories; using HopFrame.Security.Authentication; using HopFrame.Security.Claims; -using HopFrame.Security.Services; using HopFrame.Web.Services; using Microsoft.AspNetCore.Http; namespace HopFrame.Web; -public sealed class AuthMiddleware(IAuthService auth, IPermissionService perms) : IMiddleware { +public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perms) : IMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var loggedIn = await auth.IsLoggedIn(); @@ -20,14 +19,14 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionService perms) } var claims = new List { - new(HopFrameClaimTypes.AccessTokenId, token.Token), - new(HopFrameClaimTypes.UserId, token.UserId) + new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()), + new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString()) }; - var permissions = await perms.GetFullPermissions(token.UserId); + var permissions = await perms.GetFullPermissions(token.Owner); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); - context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName)); + context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName)); } await next?.Invoke(context); diff --git a/src/HopFrame.Web/Components/Administration/GroupAddModal.razor b/src/HopFrame.Web/Components/Administration/GroupAddModal.razor index d1e2723..8f432e7 100644 --- a/src/HopFrame.Web/Components/Administration/GroupAddModal.razor +++ b/src/HopFrame.Web/Components/Administration/GroupAddModal.razor @@ -6,8 +6,8 @@ @using BlazorStrap.V5 @using CurrieTechnologies.Razor.SweetAlert2 @using HopFrame.Database.Models +@using HopFrame.Database.Repositories @using HopFrame.Security.Claims -@using HopFrame.Security.Services @using HopFrame.Web.Model @@ -115,7 +115,8 @@ -@inject IPermissionService Permissions +@inject IGroupRepository Groups +@inject IPermissionRepository Permissions @inject SweetAlertService Alerts @inject ITokenContext Context @@ -133,7 +134,7 @@ private bool _isEdit; public async Task ShowAsync(PermissionGroup group = null) { - _allGroups = await Permissions.GetPermissionGroups(); + _allGroups = await Groups.GetPermissionGroups(); if (group is not null) { _group = new PermissionGroupAdd { @@ -167,7 +168,7 @@ } if (_isEdit) { - if (!(await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Context.User.Id))) { + if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) { await NoEditPermissions(); return; } @@ -176,7 +177,8 @@ } _group.Permissions.Add(new Permission { - PermissionName = _permissionToAdd + PermissionName = _permissionToAdd, + GrantedAt = DateTime.Now }); _permissionToAdd = null; @@ -184,8 +186,7 @@ private async Task RemovePermission(Permission permission) { if (_isEdit) { - var perm = await Permissions.GetPermission(permission.PermissionName, _group); - await Permissions.RemovePermission(perm); + await Permissions.RemovePermission(_group, permission.PermissionName); } _group.Permissions.Remove(permission); @@ -202,7 +203,7 @@ } if (_isEdit) { - if (!(await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Context.User.Id))) { + if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) { await NoEditPermissions(); return; } @@ -219,12 +220,12 @@ private async Task AddGroup() { if (_isEdit) { - if (!(await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Context.User.Id))) { + if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) { await NoEditPermissions(); return; } - await Permissions.EditPermissionGroup(_group); + await Groups.EditPermissionGroup(_group); if (ReloadPage is not null) await ReloadPage.Invoke(); @@ -239,7 +240,7 @@ return; } - if (!(await Permissions.HasPermission(Security.AdminPermissions.AddGroup, Context.User.Id))) { + if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.AddGroup)) { await NoAddPermissions(); return; } @@ -255,11 +256,12 @@ return; } - var dbGroup = await Permissions.CreatePermissionGroup("group." + _group.GroupName, _group.IsDefaultGroup, _group.Description); - - foreach (var permission in _group.Permissions) { - await Permissions.AddPermission(dbGroup, permission.PermissionName); - } + await Groups.CreatePermissionGroup(new PermissionGroup { + Description = _group.Description, + IsDefaultGroup = _group.IsDefaultGroup, + Permissions = _group.Permissions, + Name = "group." + _group.GroupName + }); if (ReloadPage is not null) await ReloadPage.Invoke(); diff --git a/src/HopFrame.Web/Components/Administration/UserAddModal.razor b/src/HopFrame.Web/Components/Administration/UserAddModal.razor index 96ff62b..44b47b3 100644 --- a/src/HopFrame.Web/Components/Administration/UserAddModal.razor +++ b/src/HopFrame.Web/Components/Administration/UserAddModal.razor @@ -6,8 +6,8 @@ @using BlazorStrap.V5 @using CurrieTechnologies.Razor.SweetAlert2 @using HopFrame.Database.Models +@using HopFrame.Database.Repositories @using HopFrame.Security.Claims -@using HopFrame.Security.Services @using HopFrame.Web.Model @@ -47,8 +47,9 @@ -@inject IUserService Users -@inject IPermissionService Permissions +@inject IUserRepository Users +@inject IPermissionRepository Permissions +@inject IGroupRepository Groups @inject SweetAlertService Alerts @inject ITokenContext Auth @@ -62,14 +63,14 @@ private BSModalBase _modal; public async Task ShowAsync() { - _allGroups = await Permissions.GetPermissionGroups(); + _allGroups = await Groups.GetPermissionGroups(); _allUsers = await Users.GetUsers(); await _modal.ShowAsync(); } private async Task AddUser() { - if (!(await Permissions.HasPermission(Security.AdminPermissions.AddUser, Auth.User.Id))) { + if (!(await Permissions.HasPermission(Auth.User, Security.AdminPermissions.AddUser))) { await NoAddPermissions(); return; } @@ -104,7 +105,11 @@ return; } - var user = await Users.AddUser(_user); + var user = await Users.AddUser(new User { + Username = _user.Username, + Email = _user.Email, + Password = _user.Password + }); if (!string.IsNullOrWhiteSpace(_user.Group)) { await Permissions.AddPermission(user, _user.Group); diff --git a/src/HopFrame.Web/Components/Administration/UserEditModal.razor b/src/HopFrame.Web/Components/Administration/UserEditModal.razor index 930dba6..f4bbe36 100644 --- a/src/HopFrame.Web/Components/Administration/UserEditModal.razor +++ b/src/HopFrame.Web/Components/Administration/UserEditModal.razor @@ -6,8 +6,8 @@ @using BlazorStrap.V5 @using CurrieTechnologies.Razor.SweetAlert2 @using HopFrame.Database.Models +@using HopFrame.Database.Repositories @using HopFrame.Security.Claims -@using HopFrame.Security.Services @using HopFrame.Web.Model @@ -57,7 +57,7 @@ @foreach (var group in _allGroups) { - @if (_userGroups.All(g => g.Name != group.Name)) { + @if (_userGroups?.All(g => g.Name != group.Name) == true) { } } @@ -100,8 +100,9 @@ -@inject IUserService Users -@inject IPermissionService Permissions +@inject IUserRepository Users +@inject IPermissionRepository Permissions +@inject IGroupRepository Groups @inject SweetAlertService Alerts @inject ITokenContext Auth @@ -118,19 +119,19 @@ private string _permissionToAdd; public async Task ShowAsync(User user) { - if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) { + if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) { await NoEditPermissions(); return; } _user = user; - _userGroups = await Permissions.GetUserPermissionGroups(_user); - _allGroups = await Permissions.GetPermissionGroups(); + _userGroups = await Groups.GetUserGroups(user); + _allGroups = await Groups.GetPermissionGroups(); await _modal.ShowAsync(); } private async Task AddGroup() { - if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) { + if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) { await NoEditPermissions(); return; } @@ -158,7 +159,7 @@ } private async Task RemoveGroup(PermissionGroup group) { - if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) { + if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) { await NoEditPermissions(); return; } @@ -172,7 +173,7 @@ }); if (result.IsConfirmed) { - await Permissions.RemoveGroupFromUser(_user, group); + await Permissions.RemovePermission(_user, group.Name); _userGroups.Remove(group); StateHasChanged(); @@ -186,7 +187,7 @@ } private async Task AddPermission() { - if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) { + if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) { await NoEditPermissions(); return; } @@ -200,8 +201,7 @@ return; } - await Permissions.AddPermission(_user, _permissionToAdd); - _user.Permissions.Add(await Permissions.GetPermission(_permissionToAdd, _user)); + _user.Permissions.Add(await Permissions.AddPermission(_user, _permissionToAdd)); _permissionToAdd = ""; await Alerts.FireAsync(new SweetAlertOptions { @@ -213,7 +213,7 @@ } private async Task RemovePermission(Permission perm) { - if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) { + if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) { await NoEditPermissions(); return; } @@ -227,7 +227,7 @@ }); if (result.IsConfirmed) { - await Permissions.RemovePermission(perm); + await Permissions.RemovePermission(perm.User, perm.PermissionName); _user.Permissions.Remove(perm); StateHasChanged(); @@ -241,7 +241,7 @@ } private async void EditUser() { - if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) { + if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) { await NoEditPermissions(); return; } diff --git a/src/HopFrame.Web/Components/AuthorizedView.razor b/src/HopFrame.Web/Components/AuthorizedView.razor index 5c38d4d..f87a3be 100644 --- a/src/HopFrame.Web/Components/AuthorizedView.razor +++ b/src/HopFrame.Web/Components/AuthorizedView.razor @@ -1,4 +1,4 @@ -@using HopFrame.Security.Authorization +@using HopFrame.Database @using HopFrame.Security.Claims @using Microsoft.AspNetCore.Http diff --git a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor index f6d8c68..c346c23 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor @@ -61,7 +61,7 @@ _hasError = true; return; } - + Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : RedirectAfter, true); } } \ No newline at end of file diff --git a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor.css b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor.css index b92aa21..26ceacc 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor.css +++ b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor.css @@ -6,7 +6,7 @@ .field-wrapper { margin-top: 25vh; - min-width: 30vw; + min-width: 500px; padding: 30px; border: 2px solid #ced4da; diff --git a/src/HopFrame.Web/Pages/Administration/GroupsPage.razor b/src/HopFrame.Web/Pages/Administration/GroupsPage.razor index 9040d63..4591f60 100644 --- a/src/HopFrame.Web/Pages/Administration/GroupsPage.razor +++ b/src/HopFrame.Web/Pages/Administration/GroupsPage.razor @@ -11,8 +11,8 @@ @using BlazorStrap.V5 @using CurrieTechnologies.Razor.SweetAlert2 @using HopFrame.Database.Models +@using HopFrame.Database.Repositories @using HopFrame.Security.Claims -@using HopFrame.Security.Services @using HopFrame.Web.Pages.Administration.Layout Groups @@ -94,7 +94,8 @@ -@inject IPermissionService Permissions +@inject IGroupRepository Groups +@inject IPermissionRepository Permissions @inject ITokenContext Auth @inject SweetAlertService Alerts @@ -110,23 +111,23 @@ private GroupAddModal _groupAddModal; protected override async Task OnInitializedAsync() { - _groups = await Permissions.GetPermissionGroups(); + _groups = await Groups.GetPermissionGroups(); - _hasEditPrivileges = await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Auth.User.Id); - _hasDeletePrivileges = await Permissions.HasPermission(Security.AdminPermissions.DeleteGroup, Auth.User.Id); + _hasEditPrivileges = await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditGroup); + _hasDeletePrivileges = await Permissions.HasPermission(Auth.User, Security.AdminPermissions.DeleteGroup); } private async Task Reload() { _groups = new List(); - _groups = await Permissions.GetPermissionGroups(); + _groups = await Groups.GetPermissionGroups(); OrderBy(_currentOrder, false); StateHasChanged(); } private async Task Search() { - var groups = await Permissions.GetPermissionGroups(); + var groups = await Groups.GetPermissionGroups(); if (!string.IsNullOrWhiteSpace(_searchText)) { groups = groups @@ -166,7 +167,7 @@ }); if (result.IsConfirmed) { - await Permissions.DeletePermissionGroup(group); + await Groups.DeletePermissionGroup(group); await Reload(); await Alerts.FireAsync(new SweetAlertOptions { diff --git a/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor b/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor index 233cf52..a66f311 100644 --- a/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor +++ b/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor @@ -20,7 +20,7 @@ - + Dashboard @foreach (var nav in Subpages) { diff --git a/src/HopFrame.Web/Pages/Administration/UsersPage.razor b/src/HopFrame.Web/Pages/Administration/UsersPage.razor index 0bd7e96..47b871c 100644 --- a/src/HopFrame.Web/Pages/Administration/UsersPage.razor +++ b/src/HopFrame.Web/Pages/Administration/UsersPage.razor @@ -7,12 +7,12 @@ @using CurrieTechnologies.Razor.SweetAlert2 @using HopFrame.Database.Models @using HopFrame.Security.Claims -@using HopFrame.Security.Services @using HopFrame.Web.Pages.Administration.Layout @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web @using HopFrame.Web.Components @using BlazorStrap.V5 +@using HopFrame.Database.Repositories @using HopFrame.Web.Components.Administration Users @@ -95,8 +95,9 @@ -@inject IUserService UserService -@inject IPermissionService PermissionsService +@inject IUserRepository UserService +@inject IPermissionRepository PermissionsService +@inject IGroupRepository Groups @inject SweetAlertService Alerts @inject ITokenContext Auth @@ -119,12 +120,12 @@ _users = await UserService.GetUsers(); foreach (var user in _users) { - var groups = await PermissionsService.GetUserPermissionGroups(user); + var groups = await Groups.GetUserGroups(user); _userGroups.Add(user.Id, groups.LastOrDefault()); } - _hasEditPrivileges = await PermissionsService.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id); - _hasDeletePrivileges = await PermissionsService.HasPermission(Security.AdminPermissions.DeleteUser, Auth.User.Id); + _hasEditPrivileges = await PermissionsService.HasPermission(Auth.User, Security.AdminPermissions.EditUser); + _hasDeletePrivileges = await PermissionsService.HasPermission(Auth.User, Security.AdminPermissions.DeleteUser); } private async Task Reload() { @@ -134,7 +135,7 @@ _users = await UserService.GetUsers(); foreach (var user in _users) { - var groups = await PermissionsService.GetUserPermissionGroups(user); + var groups = await Groups.GetUserGroups(user); _userGroups.Add(user.Id, groups.LastOrDefault()); } diff --git a/src/HopFrame.Web/ServiceCollectionExtensions.cs b/src/HopFrame.Web/ServiceCollectionExtensions.cs index b6770d7..a9ab84e 100644 --- a/src/HopFrame.Web/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Web/ServiceCollectionExtensions.cs @@ -12,14 +12,15 @@ namespace HopFrame.Web; public static class ServiceCollectionExtensions { public static IServiceCollection AddHopFrame(this IServiceCollection services) where TDbContext : HopDbContextBase { services.AddHttpClient(); - services.AddScoped>(); + services.AddHopFrameRepositories(); + services.AddScoped(); services.AddTransient(); // Component library's services.AddSweetAlert2(); services.AddBlazorStrap(); - services.AddHopFrameAuthentication(); + services.AddHopFrameAuthentication(); return services; } diff --git a/src/HopFrame.Web/Services/IAuthService.cs b/src/HopFrame.Web/Services/IAuthService.cs index f3e588c..218957b 100644 --- a/src/HopFrame.Web/Services/IAuthService.cs +++ b/src/HopFrame.Web/Services/IAuthService.cs @@ -1,4 +1,4 @@ -using HopFrame.Database.Models.Entries; +using HopFrame.Database.Models; using HopFrame.Security.Models; namespace HopFrame.Web.Services; @@ -8,6 +8,6 @@ public interface IAuthService { Task Login(UserLogin login); Task Logout(); - Task RefreshLogin(); + Task RefreshLogin(); Task IsLoggedIn(); } \ No newline at end of file diff --git a/src/HopFrame.Web/Services/Implementation/AuthService.cs b/src/HopFrame.Web/Services/Implementation/AuthService.cs index 682397a..6a96a97 100644 --- a/src/HopFrame.Web/Services/Implementation/AuthService.cs +++ b/src/HopFrame.Web/Services/Implementation/AuthService.cs @@ -1,47 +1,38 @@ -using HopFrame.Database; -using HopFrame.Database.Models.Entries; +using HopFrame.Database.Models; +using HopFrame.Database.Repositories; using HopFrame.Security.Authentication; using HopFrame.Security.Claims; using HopFrame.Security.Models; -using HopFrame.Security.Services; using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; namespace HopFrame.Web.Services.Implementation; -internal class AuthService( - IUserService userService, +internal class AuthService( + IUserRepository userService, IHttpContextAccessor httpAccessor, - TDbContext context) - : IAuthService where TDbContext : HopDbContextBase { + ITokenRepository tokens, + ITokenContext context) + : IAuthService { public async Task Register(UserRegister register) { - var user = await userService.AddUser(register); + var user = await userService.AddUser(new User { + Username = register.Username, + Email = register.Email, + Password = register.Password + }); + if (user is null) return; + + var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); + var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - var refreshToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.RefreshTokenType, - UserId = user.Id.ToString() - }; - var accessToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.AccessTokenType, - UserId = user.Id.ToString() - }; - - context.Tokens.AddRange(refreshToken, accessToken); - await context.SaveChangesAsync(); - - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.RefreshTokenTime, HttpOnly = true, Secure = true }); - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = false, Secure = true }); @@ -53,29 +44,16 @@ internal class AuthService( if (user == null) return false; if (await userService.CheckUserPassword(user, login.Password) == false) return false; - var refreshToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.RefreshTokenType, - UserId = user.Id.ToString() - }; - var accessToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.AccessTokenType, - UserId = user.Id.ToString() - }; + var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); + var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - context.Tokens.AddRange(refreshToken, accessToken); - await context.SaveChangesAsync(); - - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.RefreshTokenTime, HttpOnly = true, Secure = true }); - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = false, Secure = true }); @@ -84,67 +62,27 @@ internal class AuthService( } public async Task Logout() { - var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType]; - var refreshToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType]; - - var tokenEntries = await context.Tokens.Where(token => - (token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) || - (token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType)) - .ToArrayAsync(); - - context.Tokens.Remove(tokenEntries[0]); - context.Tokens.Remove(tokenEntries[1]); - await context.SaveChangesAsync(); + await tokens.DeleteUserTokens(context.User); httpAccessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType); httpAccessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType); } - public async Task RefreshLogin() { + public async Task RefreshLogin() { var refreshToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType]; if (string.IsNullOrWhiteSpace(refreshToken)) return null; - - var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType); - if (token is null) return null; + var token = await tokens.GetToken(refreshToken); - var oldAccessTokens = context.Tokens - .AsEnumerable() - .Where(old => - old.Type == TokenEntry.AccessTokenType && - old.UserId == token.UserId && - old.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) - .ToList(); - if (oldAccessTokens.Count != 0) - context.Tokens.RemoveRange(oldAccessTokens); + if (token is null || token.Type != Token.RefreshTokenType) return null; - var oldRefreshTokens = context.Tokens - .AsEnumerable() - .Where(old => - old.Type == TokenEntry.RefreshTokenType && - old.UserId == token.UserId && - old.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) - .ToList(); - if (oldRefreshTokens.Count != 0) - context.Tokens.RemoveRange(oldRefreshTokens); + if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return null; - await context.SaveChangesAsync(); + var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner); - if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return null; - - var accessToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.AccessTokenType, - UserId = token.UserId - }; - - await context.Tokens.AddAsync(accessToken); - await context.SaveChangesAsync(); - - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = false, Secure = true }); @@ -155,12 +93,13 @@ internal class AuthService( public async Task IsLoggedIn() { var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType]; if (string.IsNullOrEmpty(accessToken)) return false; - - var tokenEntry = await context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken); + + var tokenEntry = await tokens.GetToken(accessToken); if (tokenEntry is null) return false; - if (tokenEntry.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) return false; - if (!await context.Users.AnyAsync(user => user.Id == tokenEntry.UserId)) return false; + if (tokenEntry.Type != Token.AccessTokenType) return false; + if (tokenEntry.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) return false; + if (tokenEntry.Owner is null) return false; return true; } diff --git a/test/RestApiTest/Controllers/TestController.cs b/test/RestApiTest/Controllers/TestController.cs index f13bb74..092784f 100644 --- a/test/RestApiTest/Controllers/TestController.cs +++ b/test/RestApiTest/Controllers/TestController.cs @@ -1,17 +1,54 @@ +using HopFrame.Api.Logic; using HopFrame.Database.Models; using HopFrame.Security.Authorization; using HopFrame.Security.Claims; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RestApiTest.Models; namespace RestApiTest.Controllers; [ApiController] [Route("test")] -public class TestController(ITokenContext userContext) : ControllerBase { +public class TestController(ITokenContext userContext, DatabaseContext context) : ControllerBase { [HttpGet("permissions"), Authorized] public ActionResult> Permissions() { return new ActionResult>(userContext.User.Permissions); } + + [HttpGet("generate")] + public async Task GenerateData() { + var employee = new Employee() { + Name = "Max Mustermann" + }; + + await context.AddAsync(employee); + await context.SaveChangesAsync(); + + var address = new Address() { + City = "Musterstadt", + Country = "Musterland", + State = "Musterbundesland", + ZipCode = 12345, + AddressDetails = "Musterstraße 5", + Employee = employee + }; + + await context.AddAsync(address); + await context.SaveChangesAsync(); + + return LogicResult.Ok(); + } + + [HttpGet("employees")] + public async Task>> GetEmployees() { + return LogicResult>.Ok(await context.Employees.Include(e => e.Address).ToListAsync()); + } + + [HttpGet("addresses")] + public async Task>> GetAddresses() { + return LogicResult>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync()); + } } \ No newline at end of file diff --git a/test/RestApiTest/DatabaseContext.cs b/test/RestApiTest/DatabaseContext.cs index 3133353..1167277 100644 --- a/test/RestApiTest/DatabaseContext.cs +++ b/test/RestApiTest/DatabaseContext.cs @@ -1,12 +1,25 @@ using HopFrame.Database; using Microsoft.EntityFrameworkCore; +using RestApiTest.Models; namespace RestApiTest; public class DatabaseContext : HopDbContextBase { + + public DbSet Employees { get; set; } + public DbSet
Addresses { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlite("Data Source=C:\\Users\\Remote\\Documents\\Projekte\\HopFrame\\test\\RestApiTest\\bin\\Debug\\net8.0\\test.db;Mode=ReadWrite;"); } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasOne(e => e.Address) + .WithOne(a => a.Employee); + } } \ No newline at end of file diff --git a/test/RestApiTest/Models/Address.cs b/test/RestApiTest/Models/Address.cs new file mode 100644 index 0000000..386114d --- /dev/null +++ b/test/RestApiTest/Models/Address.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace RestApiTest.Models; + +public class Address { + [ForeignKey("Employee")] + public int AddressId { get; set; } + public string AddressDetails { get; set; } + public string City { get; set; } + public int ZipCode { get; set; } + public string State { get; set; } + public string Country { get; set; } + + [JsonIgnore] + public virtual Employee Employee { get; set; } +} \ No newline at end of file diff --git a/test/RestApiTest/Models/Employee.cs b/test/RestApiTest/Models/Employee.cs new file mode 100644 index 0000000..6f70edc --- /dev/null +++ b/test/RestApiTest/Models/Employee.cs @@ -0,0 +1,8 @@ +namespace RestApiTest.Models; + +public class Employee { + public int EmployeeId { get; set; } + public string Name { get; set; } + + public virtual Address Address { get; set; } +} \ No newline at end of file diff --git a/test/RestApiTest/RestApiTest.csproj b/test/RestApiTest/RestApiTest.csproj index 2549297..5c6c301 100644 --- a/test/RestApiTest/RestApiTest.csproj +++ b/test/RestApiTest/RestApiTest.csproj @@ -2,7 +2,7 @@ net8.0 - enable + disable enable