Rebuild data storage system so that database dependencies get taken into account

This commit is contained in:
2024-09-26 21:06:48 +02:00
parent 1b3ffc82ff
commit f71587d72e
47 changed files with 714 additions and 771 deletions

View File

@@ -5,7 +5,7 @@
<driver-ref>sqlite.xerial</driver-ref> <driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver> <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/RestApiTest/bin/Debug/net8.0/test.db</jdbc-url> <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/test/RestApiTest/bin/Debug/net8.0/test.db</jdbc-url>
<jdbc-additional-properties> <jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" /> <property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties> </jdbc-additional-properties>

View File

@@ -27,10 +27,11 @@ public static class ServiceCollectionExtensions {
/// <param name="services">The service provider to add the services to</param> /// <param name="services">The service provider to add the services to</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam> /// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase { public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
services.AddHopFrameRepositories<TDbContext>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IAuthLogic, AuthLogic<TDbContext>>(); services.AddScoped<IAuthLogic, AuthLogic>();
services.AddHopFrameAuthentication<TDbContext>(); services.AddHopFrameAuthentication();
} }
} }

View File

@@ -1,16 +1,14 @@
using HopFrame.Api.Models; using HopFrame.Api.Models;
using HopFrame.Database; using HopFrame.Database.Models;
using HopFrame.Database.Models.Entries; using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models; using HopFrame.Security.Models;
using HopFrame.Security.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
namespace HopFrame.Api.Logic.Implementation; namespace HopFrame.Api.Logic.Implementation;
public class AuthLogic<TDbContext>(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<LogicResult<SingleValueResult<string>>> Login(UserLogin login) { public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
var user = await users.GetUserByEmail(login.Email); var user = await users.GetUserByEmail(login.Email);
@@ -21,34 +19,21 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
if (!await users.CheckUserPassword(user, login.Password)) if (!await users.CheckUserPassword(user, login.Password))
return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct"); return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct");
var refreshToken = new TokenEntry { var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
CreatedAt = DateTime.Now, var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
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()
};
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime, MaxAge = HopFrameAuthentication.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime, MaxAge = HopFrameAuthentication.AccessTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
await context.Tokens.AddRangeAsync(refreshToken, accessToken); return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
await context.SaveChangesAsync();
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
} }
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) { public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
@@ -59,36 +44,27 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email)) if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered"); return LogicResult<SingleValueResult<string>>.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 { var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
CreatedAt = DateTime.Now, var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
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()
};
await context.Tokens.AddRangeAsync(refreshToken, accessToken); accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
await context.SaveChangesAsync(); MaxAge = HopFrameAuthentication.RefreshTokenTime,
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime, MaxAge = HopFrameAuthentication.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token); return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
} }
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() { public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
@@ -97,31 +73,26 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
if (string.IsNullOrEmpty(refreshToken)) if (string.IsNullOrEmpty(refreshToken))
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided"); return LogicResult<SingleValueResult<string>>.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<SingleValueResult<string>>.BadRequest("The provided token is not a refresh token");
if (token is null) if (token is null)
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid"); return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
if (token.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now) if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now)
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired"); return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
var accessToken = new TokenEntry { var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = token.UserId
};
await context.Tokens.AddAsync(accessToken); accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
await context.SaveChangesAsync(); MaxAge = HopFrameAuthentication.AccessTokenTime,
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token); return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
} }
public async Task<LogicResult> Logout() { public async Task<LogicResult> Logout() {
@@ -131,17 +102,7 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken)) if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
return LogicResult.Conflict("access or refresh token not provided"); return LogicResult.Conflict("access or refresh token not provided");
var tokenEntries = await context.Tokens.Where(token => await tokens.DeleteUserTokens(tokenContext.User);
(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();
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Cryptography.KeyDerivation; using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace HopFrame.Security; namespace HopFrame.Database;
public static class EncryptionManager { public static class EncryptionManager {

View File

@@ -1,4 +1,4 @@
using HopFrame.Database.Models.Entries; using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database; namespace HopFrame.Database;
@@ -8,25 +8,27 @@ namespace HopFrame.Database;
/// </summary> /// </summary>
public abstract class HopDbContextBase : DbContext { public abstract class HopDbContextBase : DbContext {
public virtual DbSet<UserEntry> Users { get; set; } public virtual DbSet<User> Users { get; set; }
public virtual DbSet<PermissionEntry> Permissions { get; set; } public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<TokenEntry> Tokens { get; set; } public virtual DbSet<Token> Tokens { get; set; }
public virtual DbSet<GroupEntry> Groups { get; set; } public virtual DbSet<PermissionGroup> Groups { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UserEntry>(); modelBuilder.Entity<User>()
modelBuilder.Entity<PermissionEntry>(); .HasMany(u => u.Tokens)
modelBuilder.Entity<TokenEntry>(); .WithOne(t => t.Owner)
modelBuilder.Entity<GroupEntry>(); .OnDelete(DeleteBehavior.Cascade);
}
/// <summary> modelBuilder.Entity<User>()
/// Gets executed when a user is deleted through the IUserService from the .HasMany(u => u.Permissions)
/// HopFrame.Security package. You can override this method to also delete .WithOne(p => p.User)
/// related user specific entries in the database .OnDelete(DeleteBehavior.Cascade);
/// </summary>
/// <param name="user"></param> modelBuilder.Entity<PermissionGroup>()
public virtual void OnUserDelete(UserEntry user) {} .HasMany(g => g.Permissions)
.WithOne(p => p.Group)
.OnDelete(DeleteBehavior.Cascade);
}
} }

View File

@@ -14,6 +14,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
</ItemGroup> </ItemGroup>

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -1,56 +0,0 @@
using HopFrame.Database.Models.Entries;
namespace HopFrame.Database.Models;
public static class ModelExtensions {
/// <summary>
/// Converts the database model to a friendly user model
/// </summary>
/// <param name="entry">the database model</param>
/// <param name="contextBase">the data source for the permissions and users</param>
/// <returns></returns>
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;
}
}

View File

@@ -1,10 +1,26 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace HopFrame.Database.Models; namespace HopFrame.Database.Models;
public sealed class Permission { public class Permission {
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; init; } public long Id { get; init; }
[Required, MaxLength(255)]
public string PermissionName { get; set; } public string PermissionName { get; set; }
public Guid Owner { get; set; }
[Required]
public DateTime GrantedAt { get; set; } 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;

View File

@@ -1,9 +1,22 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace HopFrame.Database.Models; namespace HopFrame.Database.Models;
public class PermissionGroup : IPermissionOwner { public class PermissionGroup : IPermissionOwner {
[Key, Required, MaxLength(50)]
public string Name { get; init; } public string Name { get; init; }
[Required, DefaultValue(false)]
public bool IsDefaultGroup { get; set; } public bool IsDefaultGroup { get; set; }
[MaxLength(500)]
public string Description { get; set; } public string Description { get; set; }
[Required]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public IList<Permission> Permissions { get; set; }
public virtual IList<Permission> Permissions { get; set; }
} }

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations; 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 RefreshTokenType = 0;
public const int AccessTokenType = 1; public const int AccessTokenType = 1;
@@ -15,11 +17,11 @@ public class TokenEntry {
public int Type { get; set; } public int Type { get; set; }
[Key, Required, MinLength(36), MaxLength(36)] [Key, Required, MinLength(36), MaxLength(36)]
public string Token { get; set; } public Guid Content { get; set; }
[Required, MinLength(36), MaxLength(36)]
public string UserId { get; set; }
[Required] [Required]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
[ForeignKey("UserId"), JsonIgnore]
public virtual User Owner { get; set; }
} }

View File

@@ -1,9 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace HopFrame.Database.Models; namespace HopFrame.Database.Models;
public sealed class User : IPermissionOwner { public class User : IPermissionOwner {
[Key, Required, MinLength(36), MaxLength(36)]
public Guid Id { get; init; } public Guid Id { get; init; }
[MaxLength(50)]
public string Username { get; set; } public string Username { get; set; }
[Required, MaxLength(50), EmailAddress]
public string Email { get; set; } public string Email { get; set; }
[Required, MinLength(8), MaxLength(255), JsonIgnore]
public string Password { get; set; }
[Required]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public IList<Permission> Permissions { get; set; }
public virtual IList<Permission> Permissions { get; set; }
[JsonIgnore]
public virtual IList<Token> Tokens { get; set; }
} }

View File

@@ -1,4 +1,4 @@
namespace HopFrame.Security.Authorization; namespace HopFrame.Database;
public static class PermissionValidator { public static class PermissionValidator {

View File

@@ -0,0 +1,19 @@
using HopFrame.Database.Models;
namespace HopFrame.Database.Repositories;
public interface IGroupRepository {
Task<IList<PermissionGroup>> GetPermissionGroups();
Task<IList<PermissionGroup>> GetDefaultGroups();
Task<PermissionGroup> GetPermissionGroup(string name);
Task EditPermissionGroup(PermissionGroup group);
Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group);
Task DeletePermissionGroup(PermissionGroup group);
internal Task<IList<string>> GetFullGroupPermissions(string group);
}

View File

@@ -0,0 +1,23 @@
using HopFrame.Database.Models;
namespace HopFrame.Database.Repositories;
public interface IPermissionRepository {
Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions);
/// <summary>
/// permission system:<br/>
/// - "*" -> all rights<br/>
/// - "group.[name]" -> group member<br/>
/// - "[namespace].[name]" -> single permission<br/>
/// - "[namespace].*" -> all permissions in the namespace
/// </summary>
/// <param name="owner"></param>
/// <param name="permission"></param>
/// <returns></returns>
Task<Permission> AddPermission(IPermissionOwner owner, string permission);
Task RemovePermission(IPermissionOwner owner, string permission);
public Task<IList<string>> GetFullPermissions(IPermissionOwner owner);
}

View File

@@ -0,0 +1,9 @@
using HopFrame.Database.Models;
namespace HopFrame.Database.Repositories;
public interface ITokenRepository {
public Task<Token> GetToken(string content);
public Task<Token> CreateToken(int type, User owner);
public Task DeleteUserTokens(User owner);
}

View File

@@ -1,9 +1,8 @@
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Security.Models;
namespace HopFrame.Security.Services; namespace HopFrame.Database.Repositories;
public interface IUserService { public interface IUserRepository {
Task<IList<User>> GetUsers(); Task<IList<User>> GetUsers();
Task<User> GetUser(Guid userId); Task<User> GetUser(Guid userId);
@@ -12,12 +11,12 @@ public interface IUserService {
Task<User> GetUserByUsername(string username); Task<User> GetUserByUsername(string username);
Task<User> AddUser(UserRegister user); Task<User> AddUser(User user);
/// <summary> /// <summary>
/// IMPORTANT:<br/> /// IMPORTANT:<br/>
/// This function does not add or remove any permissions to the user. /// This function does not add or remove any permissions to the user.
/// For that please use <see cref="IPermissionService"/> /// For that please use <see cref="IPermissionRepository"/>
/// </summary> /// </summary>
Task UpdateUser(User user); Task UpdateUser(User user);

View File

@@ -0,0 +1,71 @@
using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database.Repositories.Implementation;
internal sealed class GroupRepository<TDbContext>(TDbContext context) : IGroupRepository where TDbContext : HopDbContextBase {
public async Task<IList<PermissionGroup>> GetPermissionGroups() {
return await context.Groups
.Include(g => g.Permissions)
.ToListAsync();
}
public async Task<IList<PermissionGroup>> GetDefaultGroups() {
return await context.Groups
.Include(g => g.Permissions)
.Where(g => g.IsDefaultGroup)
.ToListAsync();
}
public async Task<PermissionGroup> 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<PermissionGroup> 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<IList<string>> 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;
}
}

View File

@@ -0,0 +1,89 @@
using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database.Repositories.Implementation;
internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase {
public async Task<bool> 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<Permission> 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<IList<string>> GetFullPermissions(IPermissionOwner owner) {
var permissions = new List<string>();
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;
}
}

View File

@@ -0,0 +1,41 @@
using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database.Repositories.Implementation;
internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRepository where TDbContext : HopDbContextBase {
public async Task<Token> 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<Token> 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();
}
}

View File

@@ -0,0 +1,116 @@
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>(TDbContext context, IGroupRepository groupRepository) : IUserRepository where TDbContext : HopDbContextBase {
private IIncludableQueryable<User, IList<Token>> IncludeReferences() {
return context.Users
.Include(u => u.Permissions)
.ThenInclude(p => p.Group)
.Include(u => u.Tokens);
}
public async Task<IList<User>> GetUsers() {
return await IncludeReferences()
.ToListAsync();
}
public async Task<User> GetUser(Guid userId) {
return await IncludeReferences()
.Where(u => u.Id == userId)
.SingleOrDefaultAsync();
}
public async Task<User> GetUserByEmail(string email) {
return await IncludeReferences()
.Where(u => u.Email == email)
.SingleOrDefaultAsync();
}
public async Task<User> GetUserByUsername(string username) {
return await IncludeReferences()
.Where(u => u.Username == username)
.SingleOrDefaultAsync();
}
public async Task<User> 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,
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,
//TODO: Check if user needs to be set
});
}
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<bool> 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();
}
}

View File

@@ -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<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
services.AddScoped<IGroupRepository, GroupRepository<TDbContext>>();
services.AddScoped<IPermissionRepository, PermissionRepository<TDbContext>>();
services.AddScoped<IUserRepository, UserRepository<TDbContext>>();
services.AddScoped<ITokenRepository, TokenRepository<TDbContext>>();
return services;
}
}

View File

@@ -1,10 +1,8 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using HopFrame.Database; using HopFrame.Database.Repositories;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Services;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -13,15 +11,15 @@ using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication; namespace HopFrame.Security.Authentication;
public class HopFrameAuthentication<TDbContext>( public class HopFrameAuthentication(
IOptionsMonitor<AuthenticationSchemeOptions> options, IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, ILoggerFactory logger,
UrlEncoder encoder, UrlEncoder encoder,
ISystemClock clock, ISystemClock clock,
TDbContext context, ITokenRepository tokens,
IPermissionService perms) IUserRepository users,
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) IPermissionRepository perms)
where TDbContext : HopDbContextBase { : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
public const string SchemeName = "HopCore.Authentication"; public const string SchemeName = "HopCore.Authentication";
public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0); public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
@@ -33,20 +31,20 @@ public class HopFrameAuthentication<TDbContext>(
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided"); 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 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 (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"); return AuthenticateResult.Fail("The provided Access Token does not match any user");
var claims = new List<Claim> { var claims = new List<Claim> {
new(HopFrameClaimTypes.AccessTokenId, accessToken), 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))); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
var principal = new ClaimsPrincipal(); var principal = new ClaimsPrincipal();

View File

@@ -1,7 +1,4 @@
using HopFrame.Database;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Services;
using HopFrame.Security.Services.Implementation;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -17,13 +14,11 @@ public static class HopFrameAuthenticationExtensions {
/// <param name="service">The service provider to add the services to</param> /// <param name="service">The service provider to add the services to</param>
/// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam> /// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam>
/// <returns></returns> /// <returns></returns>
public static IServiceCollection AddHopFrameAuthentication<TDbContext>(this IServiceCollection service) where TDbContext : HopDbContextBase { public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) {
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
service.AddScoped<ITokenContext, TokenContextImplementor<TDbContext>>(); service.AddScoped<ITokenContext, TokenContextImplementor>();
service.AddScoped<IPermissionService, PermissionService<TDbContext>>();
service.AddScoped<IUserService, UserService<TDbContext>>();
service.AddAuthentication(HopFrameAuthentication<TDbContext>.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication<TDbContext>>(HopFrameAuthentication<TDbContext>.SchemeName, _ => {}); service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
service.AddAuthorization(); service.AddAuthorization();
return service; return service;

View File

@@ -1,3 +1,4 @@
using HopFrame.Database;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Authorization;

View File

@@ -20,5 +20,5 @@ public interface ITokenContext {
/// <summary> /// <summary>
/// The access token the user provided /// The access token the user provided
/// </summary> /// </summary>
Guid AccessToken { get; } Token AccessToken { get; }
} }

View File

@@ -1,15 +1,13 @@
using HopFrame.Database;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace HopFrame.Security.Claims; namespace HopFrame.Security.Claims;
internal sealed class TokenContextImplementor<TDbContext>(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 bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
public User User => context.Users public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult();
.SingleOrDefault(user => user.Id == accessor.HttpContext.User.GetUserId())?
.ToUserModel(context);
public Guid AccessToken => Guid.Parse(accessor.HttpContext?.User.GetAccessTokenId() ?? Guid.Empty.ToString()); public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
} }

View File

@@ -1,48 +0,0 @@
using HopFrame.Database.Models;
namespace HopFrame.Security.Services;
/// <summary>
/// permission system:<br/>
/// - "*" -> all rights<br/>
/// - "group.[name]" -> group member<br/>
/// - "[namespace].[name]" -> single permission<br/>
/// - "[namespace].*" -> all permissions in the namespace
/// </summary>
public interface IPermissionService {
Task<bool> HasPermission(string permission, Guid user);
Task<IList<PermissionGroup>> GetPermissionGroups();
Task<PermissionGroup> GetPermissionGroup(string name);
Task EditPermissionGroup(PermissionGroup group);
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
Task RemoveGroupFromUser(User user, PermissionGroup group);
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
Task DeletePermissionGroup(PermissionGroup group);
Task<Permission> GetPermission(string name, IPermissionOwner owner);
/// <summary>
/// permission system:<br/>
/// - "*" -> all rights<br/>
/// - "group.[name]" -> group member<br/>
/// - "[namespace].[name]" -> single permission<br/>
/// - "[namespace].*" -> all permissions in the namespace
/// </summary>
/// <param name="owner"></param>
/// <param name="permission"></param>
/// <returns></returns>
Task AddPermission(IPermissionOwner owner, string permission);
Task RemovePermission(Permission permission);
Task<string[]> GetFullPermissions(string user);
}

View File

@@ -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>(TDbContext context, ITokenContext current) : IPermissionService where TDbContext : HopDbContextBase {
public async Task<bool> HasPermission(string permission) {
return await HasPermission(permission, current.User.Id);
}
public async Task<bool> 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<bool> 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<bool> HasPermission(string permission, Guid user) {
var permissions = await GetFullPermissions(user.ToString());
return PermissionValidator.IncludesPermission(permission, permissions);
}
public async Task<IList<PermissionGroup>> GetPermissionGroups() {
return await context.Groups
.Select(group => group.ToPermissionGroup(context))
.ToListAsync();
}
public Task<PermissionGroup> 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<IList<PermissionGroup>> 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<PermissionGroup> 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<Permission> 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<string[]> 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<string>();
foreach (var group in groups) {
var perms = await GetFullPermissions(group);
groupPerms.AddRange(perms);
}
permissions.AddRange(groupPerms);
return permissions.ToArray();
}
}

View File

@@ -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>(TDbContext context) : IUserService where TDbContext : HopDbContextBase {
public async Task<IList<User>> GetUsers() {
return await context.Users
.Select(user => user.ToUserModel(context))
.ToListAsync();
}
public Task<User> GetUser(Guid userId) {
var id = userId.ToString();
return context.Users
.Where(user => user.Id == id)
.Select(user => user.ToUserModel(context))
.SingleOrDefaultAsync();
}
public Task<User> GetUserByEmail(string email) {
return context.Users
.Where(user => user.Email == email)
.Select(user => user.ToUserModel(context))
.SingleOrDefaultAsync();
}
public Task<User> GetUserByUsername(string username) {
return context.Users
.Where(user => user.Username == username)
.Select(user => user.ToUserModel(context))
.SingleOrDefaultAsync();
}
public async Task<User> 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<bool> 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();
}
}

View File

@@ -1,14 +1,13 @@
using System.Security.Claims; using System.Security.Claims;
using HopFrame.Database; using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Services;
using HopFrame.Web.Services; using HopFrame.Web.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace HopFrame.Web; 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) { public async Task InvokeAsync(HttpContext context, RequestDelegate next) {
var loggedIn = await auth.IsLoggedIn(); var loggedIn = await auth.IsLoggedIn();
@@ -20,14 +19,14 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionService perms)
} }
var claims = new List<Claim> { var claims = new List<Claim> {
new(HopFrameClaimTypes.AccessTokenId, token.Token), new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()),
new(HopFrameClaimTypes.UserId, token.UserId) 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))); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication<HopDbContextBase>.SchemeName)); context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
} }
await next?.Invoke(context); await next?.Invoke(context);

View File

@@ -6,8 +6,8 @@
@using BlazorStrap.V5 @using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2 @using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models @using HopFrame.Database.Models
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims @using HopFrame.Security.Claims
@using HopFrame.Security.Services
@using HopFrame.Web.Model @using HopFrame.Web.Model
<BSModal DataId="add-group-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal"> <BSModal DataId="add-group-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
@@ -115,7 +115,8 @@
</BSForm> </BSForm>
</BSModal> </BSModal>
@inject IPermissionService Permissions @inject IGroupRepository Groups
@inject IPermissionRepository Permissions
@inject SweetAlertService Alerts @inject SweetAlertService Alerts
@inject ITokenContext Context @inject ITokenContext Context
@@ -133,7 +134,7 @@
private bool _isEdit; private bool _isEdit;
public async Task ShowAsync(PermissionGroup group = null) { public async Task ShowAsync(PermissionGroup group = null) {
_allGroups = await Permissions.GetPermissionGroups(); _allGroups = await Groups.GetPermissionGroups();
if (group is not null) { if (group is not null) {
_group = new PermissionGroupAdd { _group = new PermissionGroupAdd {
@@ -167,7 +168,7 @@
} }
if (_isEdit) { if (_isEdit) {
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Context.User.Id))) { if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) {
await NoEditPermissions(); await NoEditPermissions();
return; return;
} }
@@ -184,8 +185,7 @@
private async Task RemovePermission(Permission permission) { private async Task RemovePermission(Permission permission) {
if (_isEdit) { if (_isEdit) {
var perm = await Permissions.GetPermission(permission.PermissionName, _group); await Permissions.RemovePermission(_group, permission.PermissionName);
await Permissions.RemovePermission(perm);
} }
_group.Permissions.Remove(permission); _group.Permissions.Remove(permission);
@@ -202,7 +202,7 @@
} }
if (_isEdit) { if (_isEdit) {
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Context.User.Id))) { if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) {
await NoEditPermissions(); await NoEditPermissions();
return; return;
} }
@@ -219,12 +219,12 @@
private async Task AddGroup() { private async Task AddGroup() {
if (_isEdit) { if (_isEdit) {
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Context.User.Id))) { if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) {
await NoEditPermissions(); await NoEditPermissions();
return; return;
} }
await Permissions.EditPermissionGroup(_group); await Groups.EditPermissionGroup(_group);
if (ReloadPage is not null) if (ReloadPage is not null)
await ReloadPage.Invoke(); await ReloadPage.Invoke();
@@ -239,7 +239,7 @@
return; return;
} }
if (!(await Permissions.HasPermission(Security.AdminPermissions.AddGroup, Context.User.Id))) { if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.AddGroup)) {
await NoAddPermissions(); await NoAddPermissions();
return; return;
} }
@@ -255,11 +255,7 @@
return; return;
} }
var dbGroup = await Permissions.CreatePermissionGroup("group." + _group.GroupName, _group.IsDefaultGroup, _group.Description); await Groups.CreatePermissionGroup(_group);
foreach (var permission in _group.Permissions) {
await Permissions.AddPermission(dbGroup, permission.PermissionName);
}
if (ReloadPage is not null) if (ReloadPage is not null)
await ReloadPage.Invoke(); await ReloadPage.Invoke();

View File

@@ -6,8 +6,8 @@
@using BlazorStrap.V5 @using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2 @using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models @using HopFrame.Database.Models
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims @using HopFrame.Security.Claims
@using HopFrame.Security.Services
@using HopFrame.Web.Model @using HopFrame.Web.Model
<BSModal DataId="add-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" OnShow="() => _user = new()" @ref="_modal"> <BSModal DataId="add-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" OnShow="() => _user = new()" @ref="_modal">
@@ -47,8 +47,9 @@
</BSForm> </BSForm>
</BSModal> </BSModal>
@inject IUserService Users @inject IUserRepository Users
@inject IPermissionService Permissions @inject IPermissionRepository Permissions
@inject IGroupRepository Groups
@inject SweetAlertService Alerts @inject SweetAlertService Alerts
@inject ITokenContext Auth @inject ITokenContext Auth
@@ -62,14 +63,14 @@
private BSModalBase _modal; private BSModalBase _modal;
public async Task ShowAsync() { public async Task ShowAsync() {
_allGroups = await Permissions.GetPermissionGroups(); _allGroups = await Groups.GetPermissionGroups();
_allUsers = await Users.GetUsers(); _allUsers = await Users.GetUsers();
await _modal.ShowAsync(); await _modal.ShowAsync();
} }
private async Task AddUser() { 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(); await NoAddPermissions();
return; return;
} }
@@ -104,7 +105,11 @@
return; 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)) { if (!string.IsNullOrWhiteSpace(_user.Group)) {
await Permissions.AddPermission(user, _user.Group); await Permissions.AddPermission(user, _user.Group);

View File

@@ -6,8 +6,8 @@
@using BlazorStrap.V5 @using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2 @using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models @using HopFrame.Database.Models
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims @using HopFrame.Security.Claims
@using HopFrame.Security.Services
@using HopFrame.Web.Model @using HopFrame.Web.Model
<BSModal DataId="edit-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal"> <BSModal DataId="edit-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
@@ -100,8 +100,9 @@
</BSForm> </BSForm>
</BSModal> </BSModal>
@inject IUserService Users @inject IUserRepository Users
@inject IPermissionService Permissions @inject IPermissionRepository Permissions
@inject IGroupRepository Groups
@inject SweetAlertService Alerts @inject SweetAlertService Alerts
@inject ITokenContext Auth @inject ITokenContext Auth
@@ -118,19 +119,19 @@
private string _permissionToAdd; private string _permissionToAdd;
public async Task ShowAsync(User user) { 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(); await NoEditPermissions();
return; return;
} }
_user = user; _user = user;
_userGroups = await Permissions.GetUserPermissionGroups(_user); _userGroups = _user.Permissions.Where(p => p.PermissionName.StartsWith("group.")).Select(p => p.Group).ToList();
_allGroups = await Permissions.GetPermissionGroups(); _allGroups = await Groups.GetPermissionGroups();
await _modal.ShowAsync(); await _modal.ShowAsync();
} }
private async Task AddGroup() { 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(); await NoEditPermissions();
return; return;
} }
@@ -158,7 +159,7 @@
} }
private async Task RemoveGroup(PermissionGroup group) { 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(); await NoEditPermissions();
return; return;
} }
@@ -172,7 +173,7 @@
}); });
if (result.IsConfirmed) { if (result.IsConfirmed) {
await Permissions.RemoveGroupFromUser(_user, group); await Permissions.RemovePermission(_user, group.Name);
_userGroups.Remove(group); _userGroups.Remove(group);
StateHasChanged(); StateHasChanged();
@@ -186,7 +187,7 @@
} }
private async Task AddPermission() { 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(); await NoEditPermissions();
return; return;
} }
@@ -200,8 +201,7 @@
return; return;
} }
await Permissions.AddPermission(_user, _permissionToAdd); _user.Permissions.Add(await Permissions.AddPermission(_user, _permissionToAdd));
_user.Permissions.Add(await Permissions.GetPermission(_permissionToAdd, _user));
_permissionToAdd = ""; _permissionToAdd = "";
await Alerts.FireAsync(new SweetAlertOptions { await Alerts.FireAsync(new SweetAlertOptions {
@@ -213,7 +213,7 @@
} }
private async Task RemovePermission(Permission perm) { 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(); await NoEditPermissions();
return; return;
} }
@@ -227,7 +227,7 @@
}); });
if (result.IsConfirmed) { if (result.IsConfirmed) {
await Permissions.RemovePermission(perm); await Permissions.RemovePermission(perm.User, perm.PermissionName);
_user.Permissions.Remove(perm); _user.Permissions.Remove(perm);
StateHasChanged(); StateHasChanged();
@@ -241,7 +241,7 @@
} }
private async void EditUser() { 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(); await NoEditPermissions();
return; return;
} }

View File

@@ -1,4 +1,4 @@
@using HopFrame.Security.Authorization @using HopFrame.Database
@using HopFrame.Security.Claims @using HopFrame.Security.Claims
@using Microsoft.AspNetCore.Http @using Microsoft.AspNetCore.Http

View File

@@ -11,8 +11,8 @@
@using BlazorStrap.V5 @using BlazorStrap.V5
@using CurrieTechnologies.Razor.SweetAlert2 @using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models @using HopFrame.Database.Models
@using HopFrame.Database.Repositories
@using HopFrame.Security.Claims @using HopFrame.Security.Claims
@using HopFrame.Security.Services
@using HopFrame.Web.Pages.Administration.Layout @using HopFrame.Web.Pages.Administration.Layout
<PageTitle>Groups</PageTitle> <PageTitle>Groups</PageTitle>
@@ -94,7 +94,8 @@
</BSTBody> </BSTBody>
</BSTable> </BSTable>
@inject IPermissionService Permissions @inject IGroupRepository Groups
@inject IPermissionRepository Permissions
@inject ITokenContext Auth @inject ITokenContext Auth
@inject SweetAlertService Alerts @inject SweetAlertService Alerts
@@ -110,23 +111,23 @@
private GroupAddModal _groupAddModal; private GroupAddModal _groupAddModal;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
_groups = await Permissions.GetPermissionGroups(); _groups = await Groups.GetPermissionGroups();
_hasEditPrivileges = await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Auth.User.Id); _hasEditPrivileges = await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditGroup);
_hasDeletePrivileges = await Permissions.HasPermission(Security.AdminPermissions.DeleteGroup, Auth.User.Id); _hasDeletePrivileges = await Permissions.HasPermission(Auth.User, Security.AdminPermissions.DeleteGroup);
} }
private async Task Reload() { private async Task Reload() {
_groups = new List<PermissionGroup>(); _groups = new List<PermissionGroup>();
_groups = await Permissions.GetPermissionGroups(); _groups = await Groups.GetPermissionGroups();
OrderBy(_currentOrder, false); OrderBy(_currentOrder, false);
StateHasChanged(); StateHasChanged();
} }
private async Task Search() { private async Task Search() {
var groups = await Permissions.GetPermissionGroups(); var groups = await Groups.GetPermissionGroups();
if (!string.IsNullOrWhiteSpace(_searchText)) { if (!string.IsNullOrWhiteSpace(_searchText)) {
groups = groups groups = groups
@@ -166,7 +167,7 @@
}); });
if (result.IsConfirmed) { if (result.IsConfirmed) {
await Permissions.DeletePermissionGroup(group); await Groups.DeletePermissionGroup(group);
await Reload(); await Reload();
await Alerts.FireAsync(new SweetAlertOptions { await Alerts.FireAsync(new SweetAlertOptions {

View File

@@ -7,12 +7,12 @@
@using CurrieTechnologies.Razor.SweetAlert2 @using CurrieTechnologies.Razor.SweetAlert2
@using HopFrame.Database.Models @using HopFrame.Database.Models
@using HopFrame.Security.Claims @using HopFrame.Security.Claims
@using HopFrame.Security.Services
@using HopFrame.Web.Pages.Administration.Layout @using HopFrame.Web.Pages.Administration.Layout
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using HopFrame.Web.Components @using HopFrame.Web.Components
@using BlazorStrap.V5 @using BlazorStrap.V5
@using HopFrame.Database.Repositories
@using HopFrame.Web.Components.Administration @using HopFrame.Web.Components.Administration
<PageTitle>Users</PageTitle> <PageTitle>Users</PageTitle>
@@ -95,8 +95,8 @@
</BSTBody> </BSTBody>
</BSTable> </BSTable>
@inject IUserService UserService @inject IUserRepository UserService
@inject IPermissionService PermissionsService @inject IPermissionRepository PermissionsService
@inject SweetAlertService Alerts @inject SweetAlertService Alerts
@inject ITokenContext Auth @inject ITokenContext Auth
@@ -119,12 +119,12 @@
_users = await UserService.GetUsers(); _users = await UserService.GetUsers();
foreach (var user in _users) { foreach (var user in _users) {
var groups = await PermissionsService.GetUserPermissionGroups(user); var groups = user.Permissions.Where(p => p.PermissionName.StartsWith("group.")).Select(p => p.Group).ToList();
_userGroups.Add(user.Id, groups.LastOrDefault()); _userGroups.Add(user.Id, groups.LastOrDefault());
} }
_hasEditPrivileges = await PermissionsService.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id); _hasEditPrivileges = await PermissionsService.HasPermission(Auth.User, Security.AdminPermissions.EditUser);
_hasDeletePrivileges = await PermissionsService.HasPermission(Security.AdminPermissions.DeleteUser, Auth.User.Id); _hasDeletePrivileges = await PermissionsService.HasPermission(Auth.User, Security.AdminPermissions.DeleteUser);
} }
private async Task Reload() { private async Task Reload() {
@@ -134,7 +134,7 @@
_users = await UserService.GetUsers(); _users = await UserService.GetUsers();
foreach (var user in _users) { foreach (var user in _users) {
var groups = await PermissionsService.GetUserPermissionGroups(user); var groups = user.Permissions.Where(p => p.PermissionName.StartsWith("group.")).Select(p => p.Group).ToList();
_userGroups.Add(user.Id, groups.LastOrDefault()); _userGroups.Add(user.Id, groups.LastOrDefault());
} }

View File

@@ -12,14 +12,15 @@ namespace HopFrame.Web;
public static class ServiceCollectionExtensions { public static class ServiceCollectionExtensions {
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase { public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
services.AddHttpClient(); services.AddHttpClient();
services.AddScoped<IAuthService, AuthService<TDbContext>>(); services.AddHopFrameRepositories<TDbContext>();
services.AddScoped<IAuthService, AuthService>();
services.AddTransient<AuthMiddleware>(); services.AddTransient<AuthMiddleware>();
// Component library's // Component library's
services.AddSweetAlert2(); services.AddSweetAlert2();
services.AddBlazorStrap(); services.AddBlazorStrap();
services.AddHopFrameAuthentication<TDbContext>(); services.AddHopFrameAuthentication();
return services; return services;
} }

View File

@@ -1,4 +1,4 @@
using HopFrame.Database.Models.Entries; using HopFrame.Database.Models;
using HopFrame.Security.Models; using HopFrame.Security.Models;
namespace HopFrame.Web.Services; namespace HopFrame.Web.Services;
@@ -8,6 +8,6 @@ public interface IAuthService {
Task<bool> Login(UserLogin login); Task<bool> Login(UserLogin login);
Task Logout(); Task Logout();
Task<TokenEntry> RefreshLogin(); Task<Token> RefreshLogin();
Task<bool> IsLoggedIn(); Task<bool> IsLoggedIn();
} }

View File

@@ -1,47 +1,38 @@
using HopFrame.Database; using HopFrame.Database.Models;
using HopFrame.Database.Models.Entries; using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models; using HopFrame.Security.Models;
using HopFrame.Security.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
namespace HopFrame.Web.Services.Implementation; namespace HopFrame.Web.Services.Implementation;
internal class AuthService<TDbContext>( internal class AuthService(
IUserService userService, IUserRepository userService,
IHttpContextAccessor httpAccessor, IHttpContextAccessor httpAccessor,
TDbContext context) ITokenRepository tokens,
: IAuthService where TDbContext : HopDbContextBase { ITokenContext context)
: IAuthService {
public async Task Register(UserRegister register) { 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; if (user is null) return;
var refreshToken = new TokenEntry { var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
CreatedAt = DateTime.Now, var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
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); httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
await context.SaveChangesAsync(); MaxAge = HopFrameAuthentication.RefreshTokenTime,
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<HopDbContextBase>.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime, MaxAge = HopFrameAuthentication.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
@@ -53,29 +44,16 @@ internal class AuthService<TDbContext>(
if (user == null) return false; if (user == null) return false;
if (await userService.CheckUserPassword(user, login.Password) == false) return false; if (await userService.CheckUserPassword(user, login.Password) == false) return false;
var refreshToken = new TokenEntry { var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
CreatedAt = DateTime.Now, var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
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); httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
await context.SaveChangesAsync(); MaxAge = HopFrameAuthentication.RefreshTokenTime,
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<HopDbContextBase>.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime, MaxAge = HopFrameAuthentication.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
@@ -84,67 +62,27 @@ internal class AuthService<TDbContext>(
} }
public async Task Logout() { public async Task Logout() {
var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType]; await tokens.DeleteUserTokens(context.User);
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();
httpAccessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType); httpAccessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
httpAccessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType); httpAccessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);
} }
public async Task<TokenEntry> RefreshLogin() { public async Task<Token> RefreshLogin() {
var refreshToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType]; var refreshToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
if (string.IsNullOrWhiteSpace(refreshToken)) return null; if (string.IsNullOrWhiteSpace(refreshToken)) return null;
var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType); var token = await tokens.GetToken(refreshToken);
if (token is null) return null; if (token is null || token.Type != Token.RefreshTokenType) return null;
var oldAccessTokens = context.Tokens if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return null;
.AsEnumerable()
.Where(old =>
old.Type == TokenEntry.AccessTokenType &&
old.UserId == token.UserId &&
old.CreatedAt + HopFrameAuthentication<TDbContext>.AccessTokenTime < DateTime.Now)
.ToList();
if (oldAccessTokens.Count != 0)
context.Tokens.RemoveRange(oldAccessTokens);
var oldRefreshTokens = context.Tokens var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
.AsEnumerable()
.Where(old =>
old.Type == TokenEntry.RefreshTokenType &&
old.UserId == token.UserId &&
old.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now)
.ToList();
if (oldRefreshTokens.Count != 0)
context.Tokens.RemoveRange(oldRefreshTokens);
await context.SaveChangesAsync(); httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime,
if (token.CreatedAt + HopFrameAuthentication<TDbContext>.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<TDbContext>.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
@@ -156,11 +94,12 @@ internal class AuthService<TDbContext>(
var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType]; var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType];
if (string.IsNullOrEmpty(accessToken)) return false; 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 is null) return false;
if (tokenEntry.CreatedAt + HopFrameAuthentication<TDbContext>.AccessTokenTime < DateTime.Now) return false; if (tokenEntry.Type != Token.AccessTokenType) return false;
if (!await context.Users.AnyAsync(user => user.Id == tokenEntry.UserId)) return false; if (tokenEntry.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) return false;
if (tokenEntry.Owner is null) return false;
return true; return true;
} }

View File

@@ -1,17 +1,54 @@
using HopFrame.Api.Logic;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Security.Authorization; using HopFrame.Security.Authorization;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace RestApiTest.Controllers; namespace RestApiTest.Controllers;
[ApiController] [ApiController]
[Route("test")] [Route("test")]
public class TestController(ITokenContext userContext) : ControllerBase { public class TestController(ITokenContext userContext, DatabaseContext context) : ControllerBase {
[HttpGet("permissions"), Authorized] [HttpGet("permissions"), Authorized]
public ActionResult<IList<Permission>> Permissions() { public ActionResult<IList<Permission>> Permissions() {
return new ActionResult<IList<Permission>>(userContext.User.Permissions); return new ActionResult<IList<Permission>>(userContext.User.Permissions);
} }
[HttpGet("generate")]
public async Task<ActionResult> 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<ActionResult<IList<Employee>>> GetEmployees() {
return LogicResult<IList<Employee>>.Ok(await context.Employees.Include(e => e.Address).ToListAsync());
}
[HttpGet("addresses")]
public async Task<ActionResult<IList<Address>>> GetAddresses() {
return LogicResult<IList<Address>>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync());
}
} }

View File

@@ -1,12 +1,25 @@
using HopFrame.Database; using HopFrame.Database;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace RestApiTest; namespace RestApiTest;
public class DatabaseContext : HopDbContextBase { public class DatabaseContext : HopDbContextBase {
public DbSet<Employee> Employees { get; set; }
public DbSet<Address> Addresses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder); base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite("Data Source=C:\\Users\\Remote\\Documents\\Projekte\\HopFrame\\test\\RestApiTest\\bin\\Debug\\net8.0\\test.db;Mode=ReadWrite;"); 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<Employee>()
.HasOne(e => e.Address)
.WithOne(a => a.Employee);
}
} }

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>