Enable navigation properties for user roles, use service in place of managers

This commit is contained in:
Pat Hartl 2024-10-16 01:54:51 -05:00
parent 14df6bc95c
commit 91829f601d
19 changed files with 2906 additions and 260 deletions

View file

@ -0,0 +1,85 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddUserRoleNavigationProperties : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Roles_Users_UserId",
table: "Roles");
migrationBuilder.DropPrimaryKey(
name: "PK_UserRoles",
table: "UserRoles");
migrationBuilder.DropIndex(
name: "IX_UserRoles_RoleId",
table: "UserRoles");
migrationBuilder.DropIndex(
name: "IX_Roles_UserId",
table: "Roles");
migrationBuilder.DropColumn(
name: "UserId",
table: "Roles");
migrationBuilder.AddPrimaryKey(
name: "PK_UserRoles",
table: "UserRoles",
columns: new[] { "RoleId", "UserId" });
migrationBuilder.CreateIndex(
name: "IX_UserRoles_UserId",
table: "UserRoles",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_UserRoles",
table: "UserRoles");
migrationBuilder.DropIndex(
name: "IX_UserRoles_UserId",
table: "UserRoles");
migrationBuilder.AddColumn<Guid>(
name: "UserId",
table: "Roles",
type: "TEXT",
nullable: true);
migrationBuilder.AddPrimaryKey(
name: "PK_UserRoles",
table: "UserRoles",
columns: new[] { "UserId", "RoleId" });
migrationBuilder.CreateIndex(
name: "IX_UserRoles_RoleId",
table: "UserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "IX_Roles_UserId",
table: "Roles",
column: "UserId");
migrationBuilder.AddForeignKey(
name: "FK_Roles_Users_UserId",
table: "Roles",
column: "UserId",
principalTable: "Users",
principalColumn: "Id");
}
}
}

View file

@ -953,9 +953,6 @@ namespace LANCommander.Migrations
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedById");
@ -966,8 +963,6 @@ namespace LANCommander.Migrations
b.HasIndex("UpdatedById");
b.HasIndex("UserId");
b.ToTable("Roles", (string)null);
});
@ -1525,21 +1520,6 @@ namespace LANCommander.Migrations
b.ToTable("UserLogins", (string)null);
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserRole", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserToken", b =>
{
b.Property<Guid>("UserId")
@ -1619,6 +1599,21 @@ namespace LANCommander.Migrations
b.ToTable("RoleCollection");
});
modelBuilder.Entity("UserRoles", b =>
{
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("RoleId", "UserId");
b.HasIndex("UserId");
b.ToTable("UserRoles");
});
modelBuilder.Entity("CategoryGame", b =>
{
b.HasOne("LANCommander.Server.Data.Models.Category", null)
@ -2210,10 +2205,6 @@ namespace LANCommander.Migrations
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany("Roles")
.HasForeignKey("UserId");
b.Navigation("CreatedBy");
b.Navigation("UpdatedBy");
@ -2464,21 +2455,6 @@ namespace LANCommander.Migrations
.IsRequired();
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserRole", b =>
{
b.HasOne("LANCommander.Server.Data.Models.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserToken", b =>
{
b.HasOne("LANCommander.Server.Data.Models.User", null)
@ -2548,6 +2524,21 @@ namespace LANCommander.Migrations
.IsRequired();
});
modelBuilder.Entity("UserRoles", b =>
{
b.HasOne("LANCommander.Server.Data.Models.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LANCommander.Server.Data.Models.Category", b =>
{
b.Navigation("Children");
@ -2631,8 +2622,6 @@ namespace LANCommander.Migrations
b.Navigation("Media");
b.Navigation("PlaySessions");
b.Navigation("Roles");
});
#pragma warning restore 612, 618
}

View file

@ -228,11 +228,19 @@ namespace LANCommander.Server.Data
// Each User can have many UserTokens
b.HasMany<UserToken>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();
// Each User can have many entries in the UserRole join table
b.HasMany<UserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
});
builder.Entity<User>()
.HasMany(u => u.Roles)
.WithMany(r => r.Users)
.UsingEntity<Dictionary<string, object>>(
"UserRoles",
ur => ur.HasOne<Role>().WithMany().HasForeignKey("RoleId").OnDelete(DeleteBehavior.Cascade),
ur => ur.HasOne<User>().WithMany().HasForeignKey("UserId").OnDelete(DeleteBehavior.Cascade)
);
builder.Entity<UserClaim>(b =>
{
// Primary key
@ -279,9 +287,6 @@ namespace LANCommander.Server.Data
// The relationships between Role and other entity types
// Note that these relationships are configured with no navigation properties
// Each Role can have many entries in the UserRole join table
b.HasMany<UserRole>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();
// Each Role can have many associated RoleClaims
b.HasMany<RoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
});
@ -294,15 +299,6 @@ namespace LANCommander.Server.Data
// Maps to the AspNetRoleClaims table
b.ToTable("RoleClaims");
});
builder.Entity<UserRole>(b =>
{
// Primary key
b.HasKey(r => new { r.UserId, r.RoleId });
// Maps to the AspNetUserRoles table
b.ToTable("UserRoles");
});
#endregion
#region Server Relationships
@ -443,5 +439,7 @@ namespace LANCommander.Server.Data
public DbSet<Issue>? Issues { get; set; }
public DbSet<Page>? Pages { get; set; }
public DbSet<StorageLocation>? StorageLocations { get; set; }
public DbSet<Role>? Roles { get; set; }
public DbSet<User>? Users { get; set; }
}
}

View file

@ -8,6 +8,7 @@ namespace LANCommander.Server.Data.Models
public class Role : IdentityRole<Guid>, IBaseModel
{
public virtual ICollection<Collection> Collections { get; set; }
public virtual ICollection<User> Users { get; set; }
[Display(Name = "Created On")]
public DateTime CreatedOn { get; set; }

View file

@ -51,6 +51,7 @@ namespace LANCommander.Server.Data.Models
[JsonIgnore]
public virtual ICollection<Media>? Media { get; set; }
public virtual ICollection<Role>? Roles { get; set; }
public virtual ICollection<UserCustomField>? CustomFields { get; set; }
[JsonIgnore]
@ -61,8 +62,6 @@ namespace LANCommander.Server.Data.Models
public string? Alias { get; set; }
public virtual ICollection<Role> Roles { get; set; } = new List<Role>();
[Display(Name = "Created On")]
public DateTime CreatedOn { get; set; }

View file

@ -2,7 +2,7 @@
namespace LANCommander.Server.Services.Models
{
public class ExistingEntityResult<T> where T : BaseModel
public class ExistingEntityResult<T> where T : class, IBaseModel
{
public T Value { get; set; }
public bool Existing { get; set; }

View file

@ -1,58 +1,27 @@
using LANCommander.Server.Data.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using LANCommander.Server.Data;
using LANCommander.Server.Data.Models;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.Server.Services
{
public class RoleService : BaseService
public class RoleService : BaseDatabaseService<Role>
{
private readonly RoleManager<Role> RoleManager;
public const string AdministratorRoleName = "Administrator";
public RoleService(
ILogger<RoleService> logger,
RoleManager<Role> roleManager) : base(logger)
{
RoleManager = roleManager;
}
public async Task<IEnumerable<Role>> Get()
{
return await RoleManager.Roles.ToListAsync();
}
public async Task<Role> Get(Guid id)
{
return await RoleManager.FindByIdAsync(id.ToString());
}
Repository<Role> repository) : base(logger, repository) { }
public async Task<Role> Get(string roleName)
{
return await RoleManager.FindByNameAsync(roleName);
return await Repository.FirstOrDefault(r => r.Name == roleName);
}
public async Task<Role> Add(Role role)
public async Task<IEnumerable<User>> GetUsers(string roleName)
{
await RoleManager.CreateAsync(role);
var role = await Get(roleName);
return await Get(role.Name);
}
public async Task<Role> Update(Role role)
{
await RoleManager.UpdateAsync(role);
return await RoleManager.FindByIdAsync(role.Id.ToString());
}
public async Task Delete(Role role)
{
await RoleManager.DeleteAsync(role);
return role.Users;
}
}
}

View file

@ -0,0 +1,65 @@
using LANCommander.Server.Data;
using LANCommander.Server.Data.Models;
using Microsoft.Extensions.Logging;
namespace LANCommander.Server.Services
{
public class UserCustomFieldService : BaseDatabaseService<UserCustomField>
{
public UserCustomFieldService(
ILogger<UserCustomFieldService> logger,
Repository<UserCustomField> repository) : base(logger, repository) { }
public async Task<UserCustomField> Get(Guid userId, string name)
{
return await Repository.FirstOrDefault(cf => cf.UserId == userId && cf.Name == name);
}
public async Task Update(Guid userId, string name, string value)
{
if (name.Length > 64)
throw new ArgumentException("Field name must be 64 characters or shorter");
if (value.Length > 1024)
throw new ArgumentException("Field value must be 1024 characters or less");
var existing = await Repository.FirstOrDefault(cf => cf.UserId == userId && cf.Name == name);
if (existing.Value == value)
return;
if (existing == null)
{
await Repository.Add(new UserCustomField
{
Name = name,
Value = value
});
await Repository.SaveChanges();
}
else if (!String.IsNullOrWhiteSpace(value))
{
existing.Value = value;
Repository.Update(existing);
await Repository.SaveChanges();
}
else
{
await Delete(userId, name);
}
}
public async Task Delete(Guid userId, string name)
{
var existing = await Repository.FirstOrDefault(cf => cf.UserId == userId && cf.Name == name);
Repository.Delete(existing);
await Repository.SaveChanges();
}
}
}

View file

@ -1,67 +1,24 @@
using LANCommander.Server.Data;
using LANCommander.Server.Data.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
namespace LANCommander.Server.Services
{
public class UserService : BaseService
public class UserService : BaseDatabaseService<User>
{
private readonly Repository<UserCustomField> CustomFieldRepository;
private readonly UserManager<User> UserManager;
private readonly RoleManager<Role> RoleManager;
private readonly RoleService RoleService;
public UserService(
ILogger<UserService> logger,
Repository<UserCustomField> customFieldRepository,
UserManager<User> userManager,
RoleManager<Role> roleManager) : base(logger)
Repository<User> repository,
RoleService roleService) : base(logger, repository)
{
CustomFieldRepository = customFieldRepository;
UserManager = userManager;
RoleManager = roleManager;
}
public async Task<IEnumerable<User>> Get()
{
return UserManager.Users;
}
public async Task<User> Get(Guid id)
{
return await UserManager.FindByIdAsync(id.ToString());
RoleService = roleService;
}
public async Task<User> Get(string username)
{
return await UserManager.FindByNameAsync(username);
}
public async Task<IEnumerable<User>> GetInRole(string roleName)
{
return await UserManager.GetUsersInRoleAsync(roleName);
}
public async Task<User> Add(User user)
{
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
return await Get(user.UserName);
else
return null;
}
public async Task<User> Update(User user)
{
await UserManager.UpdateAsync(user);
return await Get(user.Id);
}
public async Task Delete(User user)
{
await UserManager.DeleteAsync(user);
return await Repository.FirstOrDefault(u => u.UserName == username);
}
public async Task<IEnumerable<Role>> GetRoles(string username)
@ -73,39 +30,32 @@ namespace LANCommander.Server.Services
public async Task<IEnumerable<Role>> GetRoles(User user)
{
var roleNames = await UserManager.GetRolesAsync(user);
var roles = new List<Role>();
foreach (var roleName in roleNames)
{
var role = await RoleManager.FindByNameAsync(roleName);
if (role != null)
roles.Add(role);
}
return roles;
}
public async Task<IEnumerable<User>> GetUsersInRole(string roleName)
{
return await UserManager.GetUsersInRoleAsync(roleName);
return user.Roles;
}
public async Task<bool> IsInRole(User user, string roleName)
{
return await UserManager.IsInRoleAsync(user, roleName);
if (user.Roles == null)
return false;
return user.Roles.Any(r => r.Name == roleName);
}
public async Task AddToRole(User user, string role)
public async Task AddToRole(User user, string roleName)
{
await UserManager.AddToRoleAsync(user, role);
var role = await RoleService.Get(roleName);
if (user.Roles == null)
user.Roles = new List<Role>();
user.Roles.Add(role);
await Update(user);
}
public async Task AddToRoles(User user, IEnumerable<string> roles)
public async Task AddToRoles(User user, IEnumerable<string> roleNames)
{
await UserManager.AddToRolesAsync(user, roles);
var roles = await RoleService.Get(r => roleNames.Contains(r.Name));
}
public async Task RemoveFromRole(User user, Role role)
@ -113,93 +63,45 @@ namespace LANCommander.Server.Services
await RemoveFromRole(user, role.Name);
}
public async Task RemoveFromRole(User user, string role)
public async Task RemoveFromRole(User user, string roleName)
{
await UserManager.RemoveFromRoleAsync(user, role);
var role = user.Roles.FirstOrDefault(u => u.Name == roleName);
if (role != null)
{
user.Roles.Remove(role);
await Update(user);
}
}
public async Task<bool> CheckPassword(User user, string password)
{
return await UserManager.CheckPasswordAsync(user, password);
// return await UserManager.CheckPasswordAsync(user, password);
return false;
}
public async Task<bool> ChangePassword(User user, string currentPassword, string newPassword)
{
var result = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword);
/*var result = await UserManager.ChangePasswordAsync(user, currentPassword, newPassword);
return result.Succeeded;
return result.Succeeded;*/
return false;
}
public async Task<bool> ChangePassword(User user, string newPassword)
{
var token = await UserManager.GeneratePasswordResetTokenAsync(user);
/*var token = await UserManager.GeneratePasswordResetTokenAsync(user);
var result = await UserManager.ResetPasswordAsync(user, token, newPassword);
return result.Succeeded;
return result.Succeeded;*/
return false;
}
public async Task SignOut()
{
}
public async Task<UserCustomField> GetCustomField(Guid userId, string name)
{
return await CustomFieldRepository.FirstOrDefault(cf => cf.UserId == userId && cf.Name == name);
}
public async Task UpdateCustomField(Guid userId, string name, string value)
{
if (name.Length > 64)
throw new ArgumentException("Field name must be 64 characters or shorter");
if (value.Length > 1024)
throw new ArgumentException("Field value must be 1024 characters or less");
var existing = await CustomFieldRepository.FirstOrDefault(cf => cf.UserId == userId && cf.Name == name);
if (existing.Value == value)
return;
if (existing == null)
{
await CustomFieldRepository.Add(new UserCustomField
{
Name = name,
Value = value
});
await CustomFieldRepository.SaveChanges();
}
else if (!String.IsNullOrWhiteSpace(value))
{
existing.Value = value;
CustomFieldRepository.Update(existing);
await CustomFieldRepository.SaveChanges();
}
else
{
await DeleteCustomField(userId, name);
}
}
public async Task DeleteCustomField(Guid userId, string name)
{
var existing = await CustomFieldRepository.FirstOrDefault(cf => cf.UserId == userId && cf.Name == name);
CustomFieldRepository.Delete(existing);
await CustomFieldRepository.SaveChanges();
}
public async Task DeleteCustomField(Guid userId, Guid id)
{
var existing = await CustomFieldRepository.FirstOrDefault(cf => cf.UserId == userId && cf.Id == id);
CustomFieldRepository.Delete(existing);
await CustomFieldRepository.SaveChanges();
}
}
}

View file

@ -8,7 +8,7 @@ using System.Linq.Expressions;
namespace LANCommander.Server.Services
{
public abstract class BaseDatabaseService<T> : BaseService where T : BaseModel
public abstract class BaseDatabaseService<T> : BaseService where T : class, IBaseModel
{
public Repository<T> Repository { get; set; }

View file

@ -24,17 +24,20 @@ namespace LANCommander.Server.Areas.Identity.Pages.Account
public class LoginModel : PageModel
{
private readonly SignInManager<User> _signInManager;
private readonly UserManager<User> _userManager;
private readonly UserService UserService;
private readonly RoleService RoleService;
private readonly ILogger<LoginModel> _logger;
public LoginModel(
SignInManager<User> signInManager,
UserManager<User> userManager,
UserService userService,
RoleService roleService,
ILogger<LoginModel> logger
)
{
_signInManager = signInManager;
_userManager = userManager;
UserService = userService;
RoleService = roleService;
_logger = logger;
}
@ -114,9 +117,9 @@ namespace LANCommander.Server.Areas.Identity.Pages.Account
if (DatabaseContext.Provider == Data.Enums.DatabaseProvider.Unknown)
return Redirect("/FirstTimeSetup");
var administrators = await _userManager.GetUsersInRoleAsync("Administrator");
var administrators = await RoleService.GetUsers(RoleService.AdministratorRoleName);
if (administrators.Count == 0)
if (administrators != null && administrators.Any())
return Redirect("/FirstTimeSetup");
return Page();
@ -139,9 +142,9 @@ namespace LANCommander.Server.Areas.Identity.Pages.Account
if (settings.Authentication.RequireApproval)
{
var user = await _userManager.FindByNameAsync(Input.UserName);
var user = await UserService.Get(Input.UserName);
if (user != null && !user.Approved && !(await _userManager.IsInRoleAsync(user, "Administrator")))
if (user != null && !user.Approved && !(await UserService.IsInRole(user, RoleService.AdministratorRoleName)))
{
ModelState.AddModelError(string.Empty, "Your account must be approved by an administrator.");
return Page();

View file

@ -18,12 +18,15 @@ namespace LANCommander.Server.Controllers.Api
public class ProfileController : BaseApiController
{
private readonly UserService UserService;
private readonly UserCustomFieldService UserCustomFieldService;
public ProfileController(
ILogger<ProfileController> logger,
UserService userService) : base(logger)
UserService userService,
UserCustomFieldService userCustomFieldService) : base(logger)
{
UserService = userService;
UserCustomFieldService = userCustomFieldService;
}
[HttpGet]
@ -112,7 +115,7 @@ namespace LANCommander.Server.Controllers.Api
{
var user = await UserService.Get(User?.Identity?.Name);
var field = await UserService.GetCustomField(user.Id, name);
var field = await UserCustomFieldService.Get(user.Id, name);
return Ok(field.Value);
}
@ -131,7 +134,7 @@ namespace LANCommander.Server.Controllers.Api
{
var user = await UserService.Get(User?.Identity?.Name);
await UserService.UpdateCustomField(user.Id, name, value);
await UserCustomFieldService.Update(user.Id, name, value);
return Ok(value);
}

View file

@ -279,6 +279,7 @@ namespace LANCommander.Server
builder.Services.AddScoped<IssueService>();
builder.Services.AddScoped<PageService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<UserCustomFieldService>();
builder.Services.AddScoped<RoleService>();
builder.Services.AddScoped<SetupService>();

View file

@ -59,9 +59,9 @@
Name = "Administrator"
});
var administrators = await UserService.GetUsersInRole("Administrator");
var administrators = await RoleService.GetUsers("Administrator");
if (administrators.Any())
if (administrators != null && administrators.Any())
NavigationManager.NavigateTo("/");
else
{

View file

@ -2,8 +2,7 @@
@inherits LayoutComponentBase
@layout BasicLayout
@inject UserService UserService
@inject RoleManager<Role> RoleManager
@inject IUserStore<User> UserStore
@inject RoleService RoleService
@inject NavigationManager NavigationManager
<GridRow Align="center" Class="ant-row-middle ant-row-space-around" Style="min-height: 100vh; margin-top: -96px;">
@ -34,17 +33,17 @@
{
if (DatabaseContext.Provider != Data.Enums.DatabaseProvider.Unknown)
{
var role = await RoleManager.FindByNameAsync("Administrator");
var role = await RoleService.Get("Administrator");
if (role == null)
await RoleManager.CreateAsync(new Role
await RoleService.Add(new Role
{
Name = "Administrator"
});
var administrators = await UserService.GetInRole("Administrator");
var administrators = await RoleService.GetUsers("Administrator");
if (administrators.Any())
if (administrators != null && administrators.Any())
NavigationManager.NavigateTo("/");
}
}

View file

@ -102,14 +102,12 @@
foreach (var role in await RoleService.Get())
{
var users = await UserService.GetUsersInRole(role.Name);
Roles.Add(new RoleViewModel()
{
Id = role.Id,
Name = role.Name,
Collections = role.Collections != null ? role.Collections.Count : 0,
Users = users != null ? users.Count() : 0
Users = role.Users.Count()
});
}

View file

@ -53,7 +53,7 @@
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
var currentAdmins = await UserService.GetUsersInRole("Administrator");
var currentAdmins = await RoleService.GetUsers("Administrator");
try
{

View file

@ -90,11 +90,13 @@
if (Directory.Exists(savePath))
saveSize = new DirectoryInfo(savePath).EnumerateFiles("*", SearchOption.AllDirectories).Sum(f => f.Length);
var roles = await UserService.GetRoles(user);
UserList.Add(new UserViewModel()
{
Id = user.Id,
UserName = user.UserName,
Roles = (await UserService.GetRoles(user)).Select(r => r.Name),
Roles = roles.Select(r => r.Name),
SavesSize = saveSize,
Approved = user.Approved
});