Update to dotnet 9.0, add user roles, MatchStats and OpenApi/Scalar in dev
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace LaDOSE.Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// A role a user can hold, stored in the pre-existing <c>applicationrole</c> table.
|
||||
/// Rows are reference data seeded by Sql/2026-08-05_roles.sql, not created at runtime.
|
||||
/// </summary>
|
||||
public class ApplicationRole
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
|
||||
@@ -14,6 +15,14 @@ namespace LaDOSE.Entity
|
||||
public string Password { get; set; }
|
||||
public byte[] PasswordHash { get; set; }
|
||||
public byte[] PasswordSalt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rows of the <c>applicationuserrole</c> join table for this user. Only populated
|
||||
/// when the query asks for it — see UserService, which includes it (and the role
|
||||
/// itself) everywhere the role matters; authorization reads this on every request.
|
||||
/// Prefer the <see cref="Roles.Names"/> / <see cref="Roles.IsAdmin"/> helpers.
|
||||
/// </summary>
|
||||
public List<ApplicationUserRole> UserRoles { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace LaDOSE.Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// The <c>applicationuserrole</c> join table. Declared explicitly rather than left
|
||||
/// implicit so its columns match the table that already exists in the database
|
||||
/// (<c>userid</c>, <c>roleid</c>, no surrogate key).
|
||||
/// </summary>
|
||||
public class ApplicationUserRole
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
public ApplicationUser User { get; set; }
|
||||
|
||||
public int RoleId { get; set; }
|
||||
public ApplicationRole Role { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ namespace LaDOSE.Entity.Context
|
||||
{
|
||||
public DbSet<Game> Game { get; set; }
|
||||
public DbSet<ApplicationUser> ApplicationUser { get; set; }
|
||||
public DbSet<ApplicationRole> ApplicationRole { get; set; }
|
||||
|
||||
public DbSet<Todo> Todo { get; set; }
|
||||
|
||||
@@ -49,6 +50,27 @@ namespace LaDOSE.Entity.Context
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
|
||||
#region Users and roles
|
||||
|
||||
// Maps onto the applicationrole / applicationuserrole tables that already exist
|
||||
// in the schema. The join table is configured explicitly rather than as a
|
||||
// many-to-many skip navigation so its columns are exactly userid + roleid,
|
||||
// with the pair as the key and no surrogate id.
|
||||
modelBuilder.Entity<ApplicationUserRole>(join =>
|
||||
{
|
||||
join.HasKey(ur => new { ur.UserId, ur.RoleId });
|
||||
|
||||
join.HasOne(ur => ur.User)
|
||||
.WithMany(u => u.UserRoles)
|
||||
.HasForeignKey(ur => ur.UserId);
|
||||
|
||||
join.HasOne(ur => ur.Role)
|
||||
.WithMany()
|
||||
.HasForeignKey(ur => ur.RoleId);
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
modelBuilder.Entity<Event>()
|
||||
.HasMany(s => s.Tournaments);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LaDOSE.Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// The role names the API knows about. They must exist as rows in
|
||||
/// <c>applicationrole</c> — see Sql/2026-08-05_roles.sql.
|
||||
/// </summary>
|
||||
public static class Roles
|
||||
{
|
||||
/// <summary>May manage user accounts.</summary>
|
||||
public const string Admin = "Admin";
|
||||
|
||||
/// <summary>May use everything else. The absence of a role means the same thing.</summary>
|
||||
public const string User = "User";
|
||||
|
||||
public static readonly string[] All = { Admin, User };
|
||||
|
||||
/// <summary>Case-insensitive, so 'admin' typed into the SQL seed still counts.</summary>
|
||||
public static bool IsAdmin(this ApplicationUser user)
|
||||
{
|
||||
return user.Names().Any(name => Admin.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The user's role names. Empty when the user holds no role, and also when the
|
||||
/// query did not include them — callers that authorize must load them.
|
||||
/// </summary>
|
||||
public static List<string> Names(this ApplicationUser user)
|
||||
{
|
||||
return user?.UserRoles?
|
||||
.Select(userRole => userRole.Role?.Name)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.OrderBy(name => name)
|
||||
.ToList()
|
||||
?? new List<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LaDOSE.Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// Aggregate over the persisted <see cref="Set"/> rows of one or more Events.
|
||||
/// Not an entity: it is never mapped by EF, it is computed in memory by
|
||||
/// StatisticsService and mapped to MatchStatsDTO by AutoMapper (same pattern as
|
||||
/// <see cref="Challonge.TournamentsResult"/>).
|
||||
/// Property names must stay identical to the DTO's, the mapping is by convention.
|
||||
/// </summary>
|
||||
public class MatchStats
|
||||
{
|
||||
/// <summary>How much of the requested scope actually has set data behind it.</summary>
|
||||
public MatchCoverage Coverage { get; set; } = new MatchCoverage();
|
||||
|
||||
public List<PlayerMatchStats> Players { get; set; } = new List<PlayerMatchStats>();
|
||||
|
||||
public List<HeadToHead> HeadToHead { get; set; } = new List<HeadToHead>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set coverage is sparse: Challonge-imported events have no sets at all, and events
|
||||
/// imported before SmashProvider.GetSets existed were never backfilled. These counters
|
||||
/// let a caller say "12 of 40 brackets have match data" instead of implying completeness.
|
||||
/// </summary>
|
||||
public class MatchCoverage
|
||||
{
|
||||
/// <summary>Requested event ids that actually exist.</summary>
|
||||
public int Events { get; set; }
|
||||
|
||||
/// <summary>Tournaments (brackets) belonging to those events.</summary>
|
||||
public int Brackets { get; set; }
|
||||
|
||||
/// <summary>Of those brackets, how many have at least one set row.</summary>
|
||||
public int BracketsWithSets { get; set; }
|
||||
|
||||
/// <summary>Total set rows in scope, including the ones skipped as unusable.</summary>
|
||||
public int Sets { get; set; }
|
||||
|
||||
/// <summary>Sets with a determinable winner (unequal scores, two distinct real players).</summary>
|
||||
public int DecidedSets { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerMatchStats
|
||||
{
|
||||
public int PlayerId { get; set; }
|
||||
|
||||
/// <summary>Gamertag, falling back to Name, else "#<id>".</summary>
|
||||
public string Player { get; set; }
|
||||
|
||||
/// <summary>Decided sets only. Always equals Wins + Losses.</summary>
|
||||
public int Sets { get; set; }
|
||||
|
||||
public int Wins { get; set; }
|
||||
public int Losses { get; set; }
|
||||
public int GamesWon { get; set; }
|
||||
public int GamesLost { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One row per unordered pair of players with at least one decided set.
|
||||
/// A is always the lower PlayerId so WinsA / WinsB are unambiguous.
|
||||
/// </summary>
|
||||
public class HeadToHead
|
||||
{
|
||||
public int PlayerAId { get; set; }
|
||||
public string PlayerA { get; set; }
|
||||
public int PlayerBId { get; set; }
|
||||
public string PlayerB { get; set; }
|
||||
public int WinsA { get; set; }
|
||||
public int WinsB { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user