From c7b24f5e65fc4fd9926ca9fabe59ff955ca6cacb Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Mon, 6 Nov 2023 10:03:28 +0100 Subject: [PATCH 01/20] feat(angular): Update maximumWarning and maximumError in angular.json - Updated the "maximumWarning" value from "500kb" to "1mb" - Updated the "maximumError" value from "1mb" to "4mb" fix(ng-bootstrap): Import NgbTooltipModule instead of NgbTooltip - Replaced the import statement for `NgbTooltip` with `NgbTooltipModule` in app.module.ts --- wowskarma.app/angular.json | 4 ++-- wowskarma.app/src/app/app.module.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/wowskarma.app/angular.json b/wowskarma.app/angular.json index 8cf0080c..2acbe42d 100644 --- a/wowskarma.app/angular.json +++ b/wowskarma.app/angular.json @@ -44,8 +44,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "1mb", + "maximumError": "4mb" }, { "type": "anyComponentStyle", diff --git a/wowskarma.app/src/app/app.module.ts b/wowskarma.app/src/app/app.module.ts index f1219e47..361d7e6b 100644 --- a/wowskarma.app/src/app/app.module.ts +++ b/wowskarma.app/src/app/app.module.ts @@ -3,7 +3,7 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserModule } from "@angular/platform-browser"; import { ServiceWorkerModule } from "@angular/service-worker"; -import { NgbCollapseModule, NgbPaginationModule, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; +import { NgbCollapseModule, NgbPaginationModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { ModActionTypeDisplayPipe } from 'src/app/services/pipes/mod-action-type-display.pipe'; import { AppRoutingModule } from "./app-routing.module"; import { AppWrapperComponent } from "./app-wrapper.component"; @@ -135,7 +135,7 @@ import { UserRolesComponent } from './shared/components/icons/user-roles/user-ro }), NgbCollapseModule, NgbPaginationModule, - NgbTooltip, + NgbTooltipModule ], providers: [ AuthService, From 55a3614f996fd6f0b9d9bda0c638c03846b29877 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Mon, 6 Nov 2023 10:03:45 +0100 Subject: [PATCH 02/20] feat(profile): Update profile component display elements - Added a CSS class to the

element for styling purposes - Adjusted spacing and alignment of elements within the

element - Updated the positioning and styling of the total karma display These changes improve the visual presentation of the player profile in the application. --- .../app/pages/player/profile/profile.component.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wowskarma.app/src/app/pages/player/profile/profile.component.html b/wowskarma.app/src/app/pages/player/profile/profile.component.html index 37eeea03..93387c52 100644 --- a/wowskarma.app/src/app/pages/player/profile/profile.component.html +++ b/wowskarma.app/src/app/pages/player/profile/profile.component.html @@ -1,9 +1,9 @@
-

+

-
+
@@ -13,11 +13,11 @@

>[{{clan.tag}}] - {{profile.username}} + {{profile.username}} - +
{{profileTotalKarma}} - +

From 47d91a29d48286372f438c3ee4a0dbed8e82f0c1 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sun, 26 Nov 2023 12:31:28 +0100 Subject: [PATCH 03/20] feat(api): update Nodsoft.WowsReplaysUnpack.ExtendedData package - Updated the version of Nodsoft.WowsReplaysUnpack.ExtendedData package from 1.1.17-gfc59a0dbe8 to 1.1.21-g89971f3a87 in WowsKarma.Api.csproj file. --- WowsKarma.Api/WowsKarma.Api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WowsKarma.Api/WowsKarma.Api.csproj b/WowsKarma.Api/WowsKarma.Api.csproj index 240f3f5d..2f4562e2 100644 --- a/WowsKarma.Api/WowsKarma.Api.csproj +++ b/WowsKarma.Api/WowsKarma.Api.csproj @@ -48,7 +48,7 @@ - + From 594222688fd23ca362a9b4501e98b3b33f726abf Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 11 Jan 2024 07:17:22 +0100 Subject: [PATCH 04/20] build(deps/api): Update target framework to .NET 8.0 + package versions - Updated the target framework from net7.0 to net8.0. - Updated package versions for Azure.Storage.Blobs, DSharpPlus, FlexLabs.EntityFrameworkCore.Upsert, Hangfire.AspNetCore, Hangfire.PostgreSql, Hangfire.Tags.PostgreSql, JetBrains.Annotations, Mapster, Mapster.DependencyInjection, Mapster.EFCore, Microsoft.ApplicationInsights.AspNetCore, Microsoft.AspNetCore.Authentication.JwtBearer, Microsoft.AspNetCore.SignalR.Protocols.MessagePack, Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson, Microsoft.CodeAnalysis.NetAnalyzers, Microsoft.EntityFrameworkCore.Design, Microsoft.Extensions.Configuration.CommandLine, Npgsql.EntityFrameworkCore.PostgreSQL, Serilog.AspNetCore, Serilog.Extensions.Logging. - Removed the package reference for System.IdentityModel.Tokens.Jwt. --- WowsKarma.Api/WowsKarma.Api.csproj | 36 ++++++++++++++---------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/WowsKarma.Api/WowsKarma.Api.csproj b/WowsKarma.Api/WowsKarma.Api.csproj index 2f4562e2..ae819ed4 100644 --- a/WowsKarma.Api/WowsKarma.Api.csproj +++ b/WowsKarma.Api/WowsKarma.Api.csproj @@ -1,10 +1,10 @@ - net7.0 + net8.0 preview - 0.17.1 - 0.17.1 + 0.17.2 + 0.17.2 Sakura Akeno Isayeki Nodsoft Systems WOWS Karma (API) @@ -23,38 +23,36 @@ - - - - - + + + + + - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - - + + + - From b9eddf44033bd67e76962d18e132c77515ac1dd5 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 12 Oct 2023 11:14:19 +0200 Subject: [PATCH 05/20] feat(ApiDbContext): update DataTypeMapper configuration - Change ApiDbContext class to be sealed - Remove static constructor in ApiDbContext class - Move DataTypeMapper configuration to a new extension method ConfigureApiDbDataSourceBuilder in ApiDbContextExtensions class - Update Startup.cs to use the new ConfigureApiDbDataSourceBuilder extension method for configuring NpgsqlDataSource --- WowsKarma.Api/Data/ApiDbContext.cs | 27 +++++++++++++++++++-------- WowsKarma.Api/Startup.cs | 7 ++++++- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/WowsKarma.Api/Data/ApiDbContext.cs b/WowsKarma.Api/Data/ApiDbContext.cs index f0365864..d3c182ed 100644 --- a/WowsKarma.Api/Data/ApiDbContext.cs +++ b/WowsKarma.Api/Data/ApiDbContext.cs @@ -9,7 +9,7 @@ namespace WowsKarma.Api.Data; -public class ApiDbContext : DbContext +public sealed class ApiDbContext : DbContext { public DbSet Clans { get; init; } public DbSet ClanMembers { get; init; } @@ -30,16 +30,11 @@ public class ApiDbContext : DbContext public DbSet PostModDeletedNotifications { get; init; } #endregion - - static ApiDbContext() + public ApiDbContext(DbContextOptions options) : base(options) { - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - NpgsqlConnection.GlobalTypeMapper.MapEnum(); + } - public ApiDbContext(DbContextOptions options) : base(options) { } - protected override void OnModelCreating(ModelBuilder modelBuilder) { foreach (Type type in modelBuilder.Model.GetEntityTypes().Where(t => t.ClrType.ImplementsInterface(typeof(ITimestamped))).Select(t => t.ClrType)) @@ -132,3 +127,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) #endregion } } + + +public static class ApiDbContextExtensions +{ + public static NpgsqlDataSourceBuilder ConfigureApiDbDataSourceBuilder(this NpgsqlDataSourceBuilder dataSourceBuilder) + { + dataSourceBuilder + .MapEnum() + .MapEnum() + .MapEnum(); + + dataSourceBuilder.EnableDynamicJson(); + + return dataSourceBuilder; + } +} \ No newline at end of file diff --git a/WowsKarma.Api/Startup.cs b/WowsKarma.Api/Startup.cs index 6ced6c85..9034bbcd 100644 --- a/WowsKarma.Api/Startup.cs +++ b/WowsKarma.Api/Startup.cs @@ -25,6 +25,7 @@ using Nodsoft.Wargaming.Api.Client.Clients.Wows; using Nodsoft.Wargaming.Api.Common; using Nodsoft.WowsReplaysUnpack.ExtendedData; +using Npgsql; using WowsKarma.Api.Data; using WowsKarma.Api.Hubs; using WowsKarma.Api.Infrastructure.Authorization; @@ -186,8 +187,12 @@ public void ConfigureServices(IServiceCollection services) string dbConnectionString = $"ApiDbConnectionString:{ApiRegion.ToRegionString()}"; int dbPoolSize = Configuration.GetValue("Database:PoolSize"); + NpgsqlDataSource apiDbDataSourceBuilder = new NpgsqlDataSourceBuilder(Configuration.GetConnectionString(dbConnectionString)) + .ConfigureApiDbDataSourceBuilder() + .Build(); + services.AddDbContextPool( - o => o.UseNpgsql(Configuration.GetConnectionString(dbConnectionString), + o => o.UseNpgsql(apiDbDataSourceBuilder, p => { p.EnableRetryOnFailure(); From 26b41e025535993f3f5956e5d4ec65e114b34e9d Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 11 Jan 2024 07:44:19 +0100 Subject: [PATCH 06/20] build(deps/api): update `Nodsoft.WowsReplaysUnpack.ExtendedData` package - Updated the version of Nodsoft.WowsReplaysUnpack.ExtendedData package from 1.1.21-g89971f3a87 to 1.1.24-g30ae630d7d in WowsKarma.Api.csproj file. --- WowsKarma.Api/WowsKarma.Api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WowsKarma.Api/WowsKarma.Api.csproj b/WowsKarma.Api/WowsKarma.Api.csproj index ae819ed4..0247aade 100644 --- a/WowsKarma.Api/WowsKarma.Api.csproj +++ b/WowsKarma.Api/WowsKarma.Api.csproj @@ -48,7 +48,7 @@ - + From 44c083cc073fcd88fb1fa64f295fe582d0f2b9be Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 11 Jan 2024 07:45:36 +0100 Subject: [PATCH 07/20] refactor: Various refactors & updates - Updated the Hangfire configuration in the Startup class to use NpgsqlConnection instead of a connection string. - Commented out the previous method of configuring Hangfire using a connection string and options object. - Changed the AddPaginationHeaders method in HttpExtensions class to use the Append method instead of Add method when adding pagination headers to the HttpResponse object. --- WowsKarma.Api/Startup.cs | 12 +++++------- WowsKarma.Api/Utilities/HttpExtensions.cs | 8 ++++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/WowsKarma.Api/Startup.cs b/WowsKarma.Api/Startup.cs index 9034bbcd..5f0e1e3e 100644 --- a/WowsKarma.Api/Startup.cs +++ b/WowsKarma.Api/Startup.cs @@ -1,17 +1,13 @@ using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Nodsoft.WowsReplaysUnpack; using System.IdentityModel.Tokens.Jwt; -using System.IO; using System.Net; using System.Reflection; using System.Text; @@ -236,8 +232,10 @@ public void ConfigureServices(IServiceCollection services) { options.TypeNameHandling = TypeNameHandling.Auto; }); - - config.UsePostgreSqlStorage(Configuration.GetConnectionString(dbConnectionString), new() { SchemaName = "hangfire", PrepareSchemaIfNecessary = true }); + + config.UsePostgreSqlStorage(options => options.UseNpgsqlConnection(Configuration.GetConnectionString(dbConnectionString))); + //config.UsePostgreSqlStorage(Configuration.GetConnectionString(dbConnectionString), new() { SchemaName = "hangfire", PrepareSchemaIfNecessary = true }); + config.UseSerilogLogProvider(); config.UseTagsWithPostgreSql(); }); @@ -336,7 +334,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) endpoints.MapHangfireDashboard("/hangfire", new() { AppPath = ApiRegion.GetRegionWebDomain(), - Authorization = new[] { HangfireDashboardAuthorizationFilter.Instance }, + Authorization = [ HangfireDashboardAuthorizationFilter.Instance ], IsReadOnlyFunc = HangfireDashboardAuthorizationFilter.IsAccessReadOnly, DashboardTitle = $"WOWS Karma API ({ApiRegion.ToRegionString()})" }); diff --git a/WowsKarma.Api/Utilities/HttpExtensions.cs b/WowsKarma.Api/Utilities/HttpExtensions.cs index 35c2d456..ea408dda 100644 --- a/WowsKarma.Api/Utilities/HttpExtensions.cs +++ b/WowsKarma.Api/Utilities/HttpExtensions.cs @@ -21,10 +21,10 @@ public static class HttpExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddPaginationHeaders(this HttpResponse response, PageMeta pageMeta) { - response.Headers.Add("Content-Page-Current", pageMeta.CurrentPage.ToString()); - response.Headers.Add("Content-Page-Size", pageMeta.PageSize.ToString()); - response.Headers.Add("Content-Page-Total", pageMeta.TotalPages.ToString()); - response.Headers.Add("Content-Items-Total", pageMeta.ItemsCount.ToString()); + response.Headers.Append("Content-Page-Current", pageMeta.CurrentPage.ToString()); + response.Headers.Append("Content-Page-Size", pageMeta.PageSize.ToString()); + response.Headers.Append("Content-Page-Total", pageMeta.TotalPages.ToString()); + response.Headers.Append("Content-Items-Total", pageMeta.ItemsCount.ToString()); } /// From 2a44118280596c5552aec185ac95ac0a08d613bc Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 11 Jan 2024 09:38:21 +0100 Subject: [PATCH 08/20] feat!: Switch timestamps from `DateTime` to `DateTimeOffset` + Migration - Updated the database context to use `DateTimeOffset` instead of `DateTime` for timestamp properties in entities implementing the `ITimestamped` interface. - Modified the `User`, `Clan`, `PlatformBan`, `Player`, and `Post` models to use `DateTimeOffset` for timestamp properties. - Adjusted the code in various places to handle the changes in timestamp types correctly. --- WowsKarma.Api/Data/ApiDbContext.cs | 4 +- WowsKarma.Api/Data/Models/Auth/User.cs | 2 +- WowsKarma.Api/Data/Models/Clan.cs | 10 +- WowsKarma.Api/Data/Models/ITimestamped.cs | 4 +- .../Models/Notifications/NotificationBase.cs | 4 +- WowsKarma.Api/Data/Models/PlatformBan.cs | 12 +- WowsKarma.Api/Data/Models/Player.cs | 28 +- WowsKarma.Api/Data/Models/Post.cs | 8 +- ...10956_AddReplayMinimapRendered.Designer.cs | 0 ...20230721210956_AddReplayMinimapRendered.cs | 0 ...0111075051_AddTimestampsOffset.Designer.cs | 589 ++++++++++++++++++ .../20240111075051_AddTimestampsOffset.cs | 22 + .../ApiDb/ApiDbContextModelSnapshot.cs | 32 +- .../Services/Authentication/UserService.cs | 2 +- WowsKarma.Api/Services/ClanService.cs | 28 +- .../Discord/ModActionWebhookService.cs | 2 +- WowsKarma.Api/Services/PlayerService.cs | 2 +- WowsKarma.Api/Services/Posts/PostService.cs | 6 +- WowsKarma.Api/Utilities/Conversions.cs | 11 +- .../Models/DTOs/Clans/ClanProfileDTO.cs | 6 +- .../DTOs/Notifications/NotificationBaseDTO.cs | 6 +- .../PlatformBanNotificationDTO.cs | 2 +- .../Models/DTOs/PlatformBanDTO.cs | 6 +- .../Models/DTOs/PlayerClanProfileDTO.cs | 2 +- WowsKarma.Common/Models/DTOs/PlayerPostDTO.cs | 4 +- .../Models/DTOs/PlayerProfileDTO.cs | 6 +- .../Models/DTOs/UserProfileFlagsDTO.cs | 2 +- WowsKarma.Common/Models/INotification.cs | 4 +- 28 files changed, 712 insertions(+), 92 deletions(-) rename WowsKarma.Api/Migrations/{ => ApiDb}/20230721210956_AddReplayMinimapRendered.Designer.cs (100%) rename WowsKarma.Api/Migrations/{ => ApiDb}/20230721210956_AddReplayMinimapRendered.cs (100%) create mode 100644 WowsKarma.Api/Migrations/ApiDb/20240111075051_AddTimestampsOffset.Designer.cs create mode 100644 WowsKarma.Api/Migrations/ApiDb/20240111075051_AddTimestampsOffset.cs diff --git a/WowsKarma.Api/Data/ApiDbContext.cs b/WowsKarma.Api/Data/ApiDbContext.cs index d3c182ed..ff80ecdb 100644 --- a/WowsKarma.Api/Data/ApiDbContext.cs +++ b/WowsKarma.Api/Data/ApiDbContext.cs @@ -40,7 +40,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) foreach (Type type in modelBuilder.Model.GetEntityTypes().Where(t => t.ClrType.ImplementsInterface(typeof(ITimestamped))).Select(t => t.ClrType)) { modelBuilder.Entity(type) - .Property(nameof(ITimestamped.CreatedAt)) + .Property(nameof(ITimestamped.CreatedAt)) .ValueGeneratedOnAdd() .HasDefaultValueSql("NOW()"); } @@ -137,7 +137,7 @@ public static NpgsqlDataSourceBuilder ConfigureApiDbDataSourceBuilder(this Npgsq .MapEnum() .MapEnum() .MapEnum(); - + dataSourceBuilder.EnableDynamicJson(); return dataSourceBuilder; diff --git a/WowsKarma.Api/Data/Models/Auth/User.cs b/WowsKarma.Api/Data/Models/Auth/User.cs index 4d5f5de5..02ce9837 100644 --- a/WowsKarma.Api/Data/Models/Auth/User.cs +++ b/WowsKarma.Api/Data/Models/Auth/User.cs @@ -14,5 +14,5 @@ public record User [Required] public Guid SeedToken { get; set; } - public DateTime LastTokenRequested { get; set; } + public DateTimeOffset LastTokenRequested { get; set; } } \ No newline at end of file diff --git a/WowsKarma.Api/Data/Models/Clan.cs b/WowsKarma.Api/Data/Models/Clan.cs index 26ff9817..3ab9f8df 100644 --- a/WowsKarma.Api/Data/Models/Clan.cs +++ b/WowsKarma.Api/Data/Models/Clan.cs @@ -3,7 +3,7 @@ namespace WowsKarma.Api.Data.Models; -public record Clan : ITimestamped +public sealed record Clan : ITimestamped { [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] public uint Id { get; init; } @@ -17,9 +17,9 @@ public record Clan : ITimestamped public bool IsDisbanded { get; set; } - public virtual List Members { get; set; } = new(); + public List Members { get; set; } = []; - public DateTime CreatedAt { get; init; } - public DateTime UpdatedAt { get; set; } - public DateTime MembersUpdatedAt { get; set; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; set; } + public DateTimeOffset MembersUpdatedAt { get; set; } } \ No newline at end of file diff --git a/WowsKarma.Api/Data/Models/ITimestamped.cs b/WowsKarma.Api/Data/Models/ITimestamped.cs index 9a3ae5ab..65e664af 100644 --- a/WowsKarma.Api/Data/Models/ITimestamped.cs +++ b/WowsKarma.Api/Data/Models/ITimestamped.cs @@ -2,6 +2,6 @@ public interface ITimestamped { - public DateTime CreatedAt { get; init; } - public DateTime UpdatedAt { get; set; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; set; } } \ No newline at end of file diff --git a/WowsKarma.Api/Data/Models/Notifications/NotificationBase.cs b/WowsKarma.Api/Data/Models/Notifications/NotificationBase.cs index b8abe734..b24fad67 100644 --- a/WowsKarma.Api/Data/Models/Notifications/NotificationBase.cs +++ b/WowsKarma.Api/Data/Models/Notifications/NotificationBase.cs @@ -14,8 +14,8 @@ public abstract record NotificationBase : INotification public abstract NotificationType Type { get; private protected init; } - public DateTime EmittedAt { get; private protected init; } = DateTime.UtcNow; - public DateTime? AcknowledgedAt { get; set; } + public DateTimeOffset EmittedAt { get; private protected init; } = DateTime.UtcNow; + public DateTimeOffset? AcknowledgedAt { get; set; } public virtual NotificationBaseDTO ToDTO() => new() { diff --git a/WowsKarma.Api/Data/Models/PlatformBan.cs b/WowsKarma.Api/Data/Models/PlatformBan.cs index 182adc0f..e772584e 100644 --- a/WowsKarma.Api/Data/Models/PlatformBan.cs +++ b/WowsKarma.Api/Data/Models/PlatformBan.cs @@ -5,26 +5,26 @@ namespace WowsKarma.Api.Data.Models; -public record PlatformBan : ITimestamped +public sealed record PlatformBan : ITimestamped { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; init; } [Required] public uint UserId { get; init; } - public virtual Player User { get; init; } + public Player User { get; init; } [Required] public uint ModId { get; init; } - public virtual Player Mod { get; init; } + public Player Mod { get; init; } [Required] public string Reason { get; set; } - public DateTime? BannedUntil { get; set; } + public DateTimeOffset? BannedUntil { get; set; } public bool Reverted { get; set; } - public DateTime CreatedAt { get; init; } - public DateTime UpdatedAt { get; set; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; set; } } diff --git a/WowsKarma.Api/Data/Models/Player.cs b/WowsKarma.Api/Data/Models/Player.cs index e8c1fede..6800abc3 100644 --- a/WowsKarma.Api/Data/Models/Player.cs +++ b/WowsKarma.Api/Data/Models/Player.cs @@ -5,7 +5,7 @@ namespace WowsKarma.Api.Data.Models; -public record Player : ITimestamped +public sealed record Player : ITimestamped { internal const int NegativeKarmaAbilityThreshold = -20; @@ -15,10 +15,10 @@ public record Player : ITimestamped public string Username { get; set; } - public DateTime CreatedAt { get; init; } - public DateTime UpdatedAt { get; set; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; set; } - public virtual ClanMember ClanMember { get; set; } + public ClanMember ClanMember { get; set; } public bool WgHidden { get; set; } @@ -29,23 +29,23 @@ public record Player : ITimestamped public int TeamplayRating { get; set; } public int CourtesyRating { get; set; } - public DateTime WgAccountCreatedAt { get; init; } - public DateTime LastBattleTime { get; set; } + public DateTimeOffset WgAccountCreatedAt { get; init; } + public DateTimeOffset LastBattleTime { get; set; } - public virtual List PostsReceived { get; init; } = new(); - public virtual List PostsSent { get; init; } = new(); + public List PostsReceived { get; init; } = []; + public List PostsSent { get; init; } = []; - public virtual List PlatformBans { get; init; } = new(); + public List PlatformBans { get; init; } = []; public bool NegativeKarmaAble => (SiteKarma + GameKarma) > NegativeKarmaAbilityThreshold; public bool PostsBanned { get; set; } public bool OptedOut { get; set; } - public DateTime OptOutChanged { get; set; } + public DateTimeOffset OptOutChanged { get; set; } public bool IsBanned() => PostsBanned - || PlatformBans?.Any(pb => !pb.Reverted && (pb.BannedUntil is null || pb.BannedUntil > DateTime.UtcNow)) is true; + || PlatformBans?.Any(pb => !pb.Reverted && (pb.BannedUntil is null || pb.BannedUntil > DateTimeOffset.Now)) is true; @@ -57,16 +57,16 @@ public bool IsBanned() { Id = value.Id, Username = value.Username, - WgAccountCreatedAt = value.WgAccountCreatedAt, + WgAccountCreatedAt = value.WgAccountCreatedAt.UtcDateTime, WgHidden = value.WgHidden, OptedOut = value.OptedOut, - OptOutChanged = value.OptOutChanged, + OptOutChanged = value.OptOutChanged.UtcDateTime, GameKarma = value.GameKarma, SiteKarma = value.SiteKarma, RatingPerformance = value.PerformanceRating, RatingTeamplay = value.TeamplayRating, RatingCourtesy = value.CourtesyRating, - LastBattleTime = value.LastBattleTime + LastBattleTime = value.LastBattleTime.UtcDateTime }; public static Player MapFromApi(Player source, Player mod) diff --git a/WowsKarma.Api/Data/Models/Post.cs b/WowsKarma.Api/Data/Models/Post.cs index 92fc95c3..4694276c 100644 --- a/WowsKarma.Api/Data/Models/Post.cs +++ b/WowsKarma.Api/Data/Models/Post.cs @@ -4,7 +4,7 @@ namespace WowsKarma.Api.Data.Models; -public record Post : ITimestamped +public sealed record Post : ITimestamped { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; init; } @@ -27,11 +27,11 @@ public record Post : ITimestamped public string Content { get; set; } public Guid? ReplayId { get; set; } - public virtual Replay Replay { get; set; } + public Replay Replay { get; set; } // Computed by DB Engine (hopefully) - public DateTime CreatedAt { get; init; } - public DateTime UpdatedAt { get; set; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; set; } public bool NegativeKarmaAble { get; internal set; } diff --git a/WowsKarma.Api/Migrations/20230721210956_AddReplayMinimapRendered.Designer.cs b/WowsKarma.Api/Migrations/ApiDb/20230721210956_AddReplayMinimapRendered.Designer.cs similarity index 100% rename from WowsKarma.Api/Migrations/20230721210956_AddReplayMinimapRendered.Designer.cs rename to WowsKarma.Api/Migrations/ApiDb/20230721210956_AddReplayMinimapRendered.Designer.cs diff --git a/WowsKarma.Api/Migrations/20230721210956_AddReplayMinimapRendered.cs b/WowsKarma.Api/Migrations/ApiDb/20230721210956_AddReplayMinimapRendered.cs similarity index 100% rename from WowsKarma.Api/Migrations/20230721210956_AddReplayMinimapRendered.cs rename to WowsKarma.Api/Migrations/ApiDb/20230721210956_AddReplayMinimapRendered.cs diff --git a/WowsKarma.Api/Migrations/ApiDb/20240111075051_AddTimestampsOffset.Designer.cs b/WowsKarma.Api/Migrations/ApiDb/20240111075051_AddTimestampsOffset.Designer.cs new file mode 100644 index 00000000..a3848d48 --- /dev/null +++ b/WowsKarma.Api/Migrations/ApiDb/20240111075051_AddTimestampsOffset.Designer.cs @@ -0,0 +1,589 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows; +using Nodsoft.WowsReplaysUnpack.Core.Models; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WowsKarma.Api.Data; +using WowsKarma.Api.Data.Models.Replays; +using WowsKarma.Common.Models; + +#nullable disable + +namespace WowsKarma.Api.Migrations.ApiDb +{ + [DbContext(typeof(ApiDbContext))] + [Migration("20240111075051_AddTimestampsOffset")] + partial class AddTimestampsOffset + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "clan_role", new[] { "unknown", "commander", "executive_officer", "recruitment_officer", "commissioned_officer", "officer", "private" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "mod_action_type", new[] { "deletion", "update" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "notification_type", new[] { "unknown", "other", "post_added", "post_edited", "post_deleted", "post_mod_edited", "post_mod_deleted", "platform_ban" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Clan", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDisbanded") + .HasColumnType("boolean"); + + b.Property("LeagueColor") + .HasColumnType("bigint"); + + b.Property("MembersUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Tag") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Clans"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.ClanMember", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ClanId") + .HasColumnType("bigint"); + + b.Property("JoinedAt") + .HasColumnType("date"); + + b.Property("LeftAt") + .HasColumnType("date"); + + b.Property("Role") + .HasColumnType("clan_role"); + + b.HasKey("PlayerId"); + + b.HasIndex("ClanId"); + + b.ToTable("ClanMembers"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.NotificationBase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AcknowledgedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("notification_type"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("Notifications"); + + b.HasDiscriminator("Type").IsComplete(false).HasValue(NotificationType.Unknown); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.PlatformBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BannedUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ModId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reverted") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ModId"); + + b.HasIndex("UserId"); + + b.ToTable("PlatformBans"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Player", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CourtesyRating") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("GameKarma") + .HasColumnType("integer"); + + b.Property("LastBattleTime") + .HasColumnType("timestamp with time zone"); + + b.Property("OptOutChanged") + .HasColumnType("timestamp with time zone"); + + b.Property("OptedOut") + .HasColumnType("boolean"); + + b.Property("PerformanceRating") + .HasColumnType("integer"); + + b.Property("PostsBanned") + .HasColumnType("boolean"); + + b.Property("SiteKarma") + .HasColumnType("integer"); + + b.Property("TeamplayRating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .HasColumnType("text"); + + b.Property("WgAccountCreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WgHidden") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Flairs") + .HasColumnType("integer"); + + b.Property("ModLocked") + .HasColumnType("boolean"); + + b.Property("NegativeKarmaAble") + .HasColumnType("boolean"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ReplayId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PlayerId"); + + b.HasIndex("ReplayId") + .IsUnique(); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.PostModAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasColumnType("mod_action_type"); + + b.Property("ModId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ModId"); + + b.HasIndex("PostId"); + + b.ToTable("PostModActions"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Replays.Replay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ArenaInfo") + .HasColumnType("jsonb"); + + b.Property("BlobName") + .HasColumnType("text"); + + b.Property>("ChatMessages") + .HasColumnType("jsonb"); + + b.Property("MinimapRendered") + .HasColumnType("boolean"); + + b.Property>("Players") + .HasColumnType("jsonb"); + + b.Property("PostId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Replays"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PlatformBanNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("BanId") + .HasColumnType("uuid"); + + b.HasIndex("BanId"); + + b.HasDiscriminator().HasValue(NotificationType.PlatformBan); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostAddedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("PostId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid"); + + b.HasIndex("PostId"); + + b.HasDiscriminator().HasValue(NotificationType.PostAdded); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostDeletedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("PostId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid"); + + b.HasIndex("PostId"); + + b.HasDiscriminator().HasValue(NotificationType.PostDeleted); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostEditedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("PostId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid"); + + b.HasIndex("PostId"); + + b.HasDiscriminator().HasValue(NotificationType.PostEdited); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostModDeletedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("ModActionId") + .HasColumnType("uuid"); + + b.HasIndex("ModActionId"); + + b.HasDiscriminator().HasValue(NotificationType.PostModDeleted); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostModEditedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("ModActionId") + .HasColumnType("uuid"); + + b.HasIndex("ModActionId"); + + b.ToTable("Notifications", t => + { + t.Property("ModActionId") + .HasColumnName("PostModEditedNotification_ModActionId"); + }); + + b.HasDiscriminator().HasValue(NotificationType.PostModEdited); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.ClanMember", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Clan", "Clan") + .WithMany("Members") + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WowsKarma.Api.Data.Models.Player", "Player") + .WithOne("ClanMember") + .HasForeignKey("WowsKarma.Api.Data.Models.ClanMember", "PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Clan"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.NotificationBase", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Player", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.PlatformBan", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Player", "Mod") + .WithMany() + .HasForeignKey("ModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WowsKarma.Api.Data.Models.Player", "User") + .WithMany("PlatformBans") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Mod"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Post", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Player", "Author") + .WithMany("PostsSent") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("WowsKarma.Api.Data.Models.Player", "Player") + .WithMany("PostsReceived") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("WowsKarma.Api.Data.Models.Replays.Replay", "Replay") + .WithOne("Post") + .HasForeignKey("WowsKarma.Api.Data.Models.Post", "ReplayId"); + + b.Navigation("Author"); + + b.Navigation("Player"); + + b.Navigation("Replay"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.PostModAction", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Player", "Mod") + .WithMany() + .HasForeignKey("ModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WowsKarma.Api.Data.Models.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Mod"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PlatformBanNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.PlatformBan", "Ban") + .WithMany() + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostAddedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostDeletedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostEditedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostModDeletedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.PostModAction", "ModAction") + .WithMany() + .HasForeignKey("ModActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModAction"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostModEditedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.PostModAction", "ModAction") + .WithMany() + .HasForeignKey("ModActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModAction"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Clan", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Player", b => + { + b.Navigation("ClanMember"); + + b.Navigation("PlatformBans"); + + b.Navigation("PostsReceived"); + + b.Navigation("PostsSent"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Replays.Replay", b => + { + b.Navigation("Post"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WowsKarma.Api/Migrations/ApiDb/20240111075051_AddTimestampsOffset.cs b/WowsKarma.Api/Migrations/ApiDb/20240111075051_AddTimestampsOffset.cs new file mode 100644 index 00000000..9f6612f7 --- /dev/null +++ b/WowsKarma.Api/Migrations/ApiDb/20240111075051_AddTimestampsOffset.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WowsKarma.Api.Migrations.ApiDb +{ + /// + public partial class AddTimestampsOffset : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/WowsKarma.Api/Migrations/ApiDb/ApiDbContextModelSnapshot.cs b/WowsKarma.Api/Migrations/ApiDb/ApiDbContextModelSnapshot.cs index 9ceaff2b..21a452e0 100644 --- a/WowsKarma.Api/Migrations/ApiDb/ApiDbContextModelSnapshot.cs +++ b/WowsKarma.Api/Migrations/ApiDb/ApiDbContextModelSnapshot.cs @@ -22,7 +22,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("ProductVersion", "8.0.1") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "clan_role", new[] { "unknown", "commander", "executive_officer", "recruitment_officer", "commissioned_officer", "officer", "private" }); @@ -35,7 +35,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("bigint"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("NOW()"); @@ -49,7 +49,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LeagueColor") .HasColumnType("bigint"); - b.Property("MembersUpdatedAt") + b.Property("MembersUpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("Name") @@ -58,7 +58,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Tag") .HasColumnType("text"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -99,10 +99,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AccountId") .HasColumnType("bigint"); - b.Property("AcknowledgedAt") + b.Property("AcknowledgedAt") .HasColumnType("timestamp with time zone"); - b.Property("EmittedAt") + b.Property("EmittedAt") .HasColumnType("timestamp with time zone"); b.Property("Type") @@ -125,10 +125,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("BannedUntil") + b.Property("BannedUntil") .HasColumnType("timestamp with time zone"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("NOW()"); @@ -143,7 +143,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Reverted") .HasColumnType("boolean"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("UserId") @@ -166,7 +166,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CourtesyRating") .HasColumnType("integer"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("NOW()"); @@ -174,10 +174,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("GameKarma") .HasColumnType("integer"); - b.Property("LastBattleTime") + b.Property("LastBattleTime") .HasColumnType("timestamp with time zone"); - b.Property("OptOutChanged") + b.Property("OptOutChanged") .HasColumnType("timestamp with time zone"); b.Property("OptedOut") @@ -195,13 +195,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("TeamplayRating") .HasColumnType("integer"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("Username") .HasColumnType("text"); - b.Property("WgAccountCreatedAt") + b.Property("WgAccountCreatedAt") .HasColumnType("timestamp with time zone"); b.Property("WgHidden") @@ -225,7 +225,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("NOW()"); @@ -252,7 +252,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); diff --git a/WowsKarma.Api/Services/Authentication/UserService.cs b/WowsKarma.Api/Services/Authentication/UserService.cs index 7338b391..54b70477 100644 --- a/WowsKarma.Api/Services/Authentication/UserService.cs +++ b/WowsKarma.Api/Services/Authentication/UserService.cs @@ -43,7 +43,7 @@ public async Task GetUserSeedTokenAsync(uint id) await context.Users.AddAsync(user); } - user.LastTokenRequested = DateTime.UtcNow; + user.LastTokenRequested = DateTimeOffset.UtcNow; await context.SaveChangesAsync(); return user.SeedToken; } diff --git a/WowsKarma.Api/Services/ClanService.cs b/WowsKarma.Api/Services/ClanService.cs index 29605e30..6be57dfa 100644 --- a/WowsKarma.Api/Services/ClanService.cs +++ b/WowsKarma.Api/Services/ClanService.cs @@ -75,17 +75,23 @@ public async Task GetClanAsync(uint clanId, bool includeMembers = false, C internal async Task UpdateClanInfoAsync(ApiDbContext context, uint clanId, Clan clan, CancellationToken ct) { ClanInfo apiClan = (await _clansApi.FetchClanViewAsync(clanId, ct))?.Clan; - clan = clan is null - ? apiClan?.Adapt() - : clan with + + if (clan is null) + { + clan = apiClan?.Adapt(); + } + else + { + clan = clan with { Tag = apiClan.Tag, Name = apiClan.Name, Description = apiClan.Description, LeagueColor = (uint)ColorTranslator.FromHtml(apiClan.Color).ToArgb() }; + } - clan!.UpdatedAt = DateTime.UtcNow; + clan!.UpdatedAt = DateTimeOffset.UtcNow; await context.Clans.Upsert(clan).On(c => c.Id).RunAsync(ct); return clan; @@ -111,26 +117,26 @@ internal async Task UpdateClanMembersAsync(ApiDbContext context, Clan clan foreach (uint id in outdated) { - Player dbPlayer = Player.MapFromApi(players[id], await context.Players.FindAsync(new object[] { id }, ct)); - dbPlayer.UpdatedAt = DateTime.UtcNow; + Player dbPlayer = Player.MapFromApi(players[id], await context.Players.FindAsync([id], ct)); + dbPlayer.UpdatedAt = DateTimeOffset.UtcNow; players[id] = dbPlayer; context.Update(players[id]); } - clan.Members = new(members.Values.Select(x => new ClanMember + clan.Members = [..members.Values.Select(x => new ClanMember { PlayerId = x.Id, Player = players[x.Id], ClanId = clan.Id, JoinedAt = DateOnly.FromDateTime(DateTime.UtcNow - TimeSpan.FromDays(x.DaysInClan)), Role = x.Role.Name - })); + })]; context.RemoveRange(clan.Members.Where(x => x.ClanId == clan.Id && !members.ContainsKey(x.PlayerId))); await context.SaveChangesAsync(ct); - clan.MembersUpdatedAt = DateTime.UtcNow; + clan.MembersUpdatedAt = DateTimeOffset.UtcNow; // await context.Clans.Upsert(clan).RunAsync(ct); await context.ClanMembers.UpsertRange(clan.Members).On(c => c.PlayerId).RunAsync(ct); @@ -138,6 +144,6 @@ internal async Task UpdateClanMembersAsync(ApiDbContext context, Clan clan return clan; } - public static bool ClanInfoUpdateNeeded(Clan clan) => clan.UpdatedAt + ClanInfoUpdateSpan < DateTime.UtcNow; - public static bool ClanMembersUpdateNeeded(Clan clan) => clan.MembersUpdatedAt + ClanMemberUpdateSpan < DateTime.UtcNow; + public static bool ClanInfoUpdateNeeded(Clan clan) => clan.UpdatedAt + ClanInfoUpdateSpan < DateTimeOffset.UtcNow; + public static bool ClanMembersUpdateNeeded(Clan clan) => clan.MembersUpdatedAt + ClanMemberUpdateSpan < DateTimeOffset.UtcNow; } \ No newline at end of file diff --git a/WowsKarma.Api/Services/Discord/ModActionWebhookService.cs b/WowsKarma.Api/Services/Discord/ModActionWebhookService.cs index 4774204f..d3c7d898 100644 --- a/WowsKarma.Api/Services/Discord/ModActionWebhookService.cs +++ b/WowsKarma.Api/Services/Discord/ModActionWebhookService.cs @@ -73,7 +73,7 @@ public async Task SendPlatformBanWebhookAsync(PlatformBan ban) if (ban.BannedUntil is not null) { - embed.AddField("Until", $""); + embed.AddField("Until", $""); } await Client.BroadcastMessageAsync(GetCurrentRegionWebhookBuilder() diff --git a/WowsKarma.Api/Services/PlayerService.cs b/WowsKarma.Api/Services/PlayerService.cs index 1949b86c..45df2ffd 100644 --- a/WowsKarma.Api/Services/PlayerService.cs +++ b/WowsKarma.Api/Services/PlayerService.cs @@ -267,7 +267,7 @@ public async Task RecalculatePlayerMetrics(uint playerId, CancellationToken ct) } internal static bool UpdateNeeded(Player player) => player.UpdatedAt + DataUpdateSpan < DateTime.UtcNow; - internal static bool IsOptOutOnCooldown(DateTime lastChange) => lastChange + OptOutCooldownSpan > DateTime.UtcNow; + internal static bool IsOptOutOnCooldown(DateTimeOffset lastChange) => lastChange + OptOutCooldownSpan > DateTimeOffset.UtcNow; private static void SetPlayerMetrics(Player player, int site, int performance, int teamplay, int courtesy) { diff --git a/WowsKarma.Api/Services/Posts/PostService.cs b/WowsKarma.Api/Services/Posts/PostService.cs index d4948b83..8b67770a 100644 --- a/WowsKarma.Api/Services/Posts/PostService.cs +++ b/WowsKarma.Api/Services/Posts/PostService.cs @@ -166,7 +166,7 @@ public async Task EditPostAsync(Guid id, PlayerPostDTO edited, bool modEditLock current.Title = edited.Title; current.Content = edited.Content; current.Flairs = edited.Flairs; - current.UpdatedAt = DateTime.UtcNow; // Forcing UpdatedAt refresh + current.UpdatedAt = DateTimeOffset.Now; // Forcing UpdatedAt refresh current.ReadOnly = current.ReadOnly || modEditLock; KarmaService.UpdatePlayerKarma(player, current.ParsedFlairs, previousFlairs, current.NegativeKarmaAble); @@ -239,8 +239,8 @@ from p in _context.Posts if (lastAuthoredPost is { CreatedAt: not null }) { - DateTime endsAt = lastAuthoredPost.CreatedAt.Value.Add(CooldownPeriod); - return endsAt > DateTime.UtcNow; + DateTimeOffset endsAt = lastAuthoredPost.CreatedAt.Value.Add(CooldownPeriod); + return endsAt > DateTimeOffset.UtcNow; } } diff --git a/WowsKarma.Api/Utilities/Conversions.cs b/WowsKarma.Api/Utilities/Conversions.cs index 87c2ea79..16bc1fdd 100644 --- a/WowsKarma.Api/Utilities/Conversions.cs +++ b/WowsKarma.Api/Utilities/Conversions.cs @@ -1,4 +1,5 @@ using System.Drawing; +using System.Linq.Expressions; using Mapster; using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows.Public; using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows.Vortex; @@ -11,6 +12,8 @@ public static class Conversions { public static void ConfigureMapping() { + TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileWithDebugInfo(); + TypeAdapterConfig .NewConfig() .IgnoreNullValues(true) @@ -30,7 +33,7 @@ public static void ConfigureMapping() ) .Map(dest => dest.Author.Clan, src => src.Author.ClanMember.Clan) .Map(dest => dest.Player.Clan, src => src.Player.ClanMember.Clan); - + TypeAdapterConfig .NewConfig() .Ignore(dest => dest.Post) @@ -40,7 +43,7 @@ public static void ConfigureMapping() .NewConfig() .IgnoreNullValues(true) .Map(dest => dest.LeagueColor, src => (uint)ColorTranslator.FromHtml(src.Color).ToArgb()) - .Map(dest => dest.CreatedAt, src => src.CreatedAt.UtcDateTime) + .Map(dest => dest.CreatedAt, src => src.CreatedAt.ToUniversalTime()) .Ignore(dest => dest.UpdatedAt); TypeAdapterConfig @@ -70,7 +73,6 @@ public static void ConfigureMapping() .NewConfig() .IgnoreNullValues(true) .Map(dest => dest.Members, src => src.Members) - .Fork(fork => fork.ForType() .Ignore(dest => dest.ClanInfo)); @@ -81,6 +83,9 @@ public static void ConfigureMapping() TypeAdapterConfig.NewConfig().MapWith(x => DateOnly.FromDateTime(x)); TypeAdapterConfig.NewConfig().MapWith(x => x == null ? DateTime.UnixEpoch : x.Value.ToDateTime(TimeOnly.MinValue)); + + TypeAdapterConfig.NewConfig().MapWith(x => DateOnly.FromDateTime(x.UtcDateTime)); + TypeAdapterConfig.NewConfig().MapWith(x => x == null ? DateTimeOffset.UnixEpoch : new(x.Value.ToDateTime(TimeOnly.MinValue), TimeSpan.Zero)); } public static AccountListingDTO ToDTO(this AccountListing accountListing) => new(accountListing.AccountId, accountListing.Nickname); diff --git a/WowsKarma.Common/Models/DTOs/Clans/ClanProfileDTO.cs b/WowsKarma.Common/Models/DTOs/Clans/ClanProfileDTO.cs index 3848d407..527620db 100644 --- a/WowsKarma.Common/Models/DTOs/Clans/ClanProfileDTO.cs +++ b/WowsKarma.Common/Models/DTOs/Clans/ClanProfileDTO.cs @@ -6,13 +6,13 @@ public record ClanProfileDTO : ClanListingDTO public bool IsDisbanded { get; init; } - public DateTime CreatedAt { get; init; } - public DateTime UpdatedAt { get; init; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; init; } } public record ClanProfileFullDTO : ClanProfileDTO { public virtual List Members { get; init; } = new(); - public DateTime MembersUpdatedAt { get; init; } + public DateTimeOffset MembersUpdatedAt { get; init; } } \ No newline at end of file diff --git a/WowsKarma.Common/Models/DTOs/Notifications/NotificationBaseDTO.cs b/WowsKarma.Common/Models/DTOs/Notifications/NotificationBaseDTO.cs index 39c938cf..ce07afdf 100644 --- a/WowsKarma.Common/Models/DTOs/Notifications/NotificationBaseDTO.cs +++ b/WowsKarma.Common/Models/DTOs/Notifications/NotificationBaseDTO.cs @@ -14,10 +14,8 @@ public record NotificationBaseDTO : INotification public NotificationType Type { get; init; } - public DateTime EmittedAt { get; init; } - - public DateTime? AcknowledgedAt { get; init; } - + public DateTimeOffset EmittedAt { get; init; } + public DateTimeOffset? AcknowledgedAt { get; init; } } } diff --git a/WowsKarma.Common/Models/DTOs/Notifications/PlatformBanNotificationDTO.cs b/WowsKarma.Common/Models/DTOs/Notifications/PlatformBanNotificationDTO.cs index 83dc5d89..6948da69 100644 --- a/WowsKarma.Common/Models/DTOs/Notifications/PlatformBanNotificationDTO.cs +++ b/WowsKarma.Common/Models/DTOs/Notifications/PlatformBanNotificationDTO.cs @@ -3,5 +3,5 @@ public record PlatformBanNotificationDTO : NotificationBaseDTO { public string Reason { get; set; } = string.Empty; - public DateTime? Until { get; set; } + public DateTimeOffset? Until { get; set; } } diff --git a/WowsKarma.Common/Models/DTOs/PlatformBanDTO.cs b/WowsKarma.Common/Models/DTOs/PlatformBanDTO.cs index 4a745ea1..a25ed3a5 100644 --- a/WowsKarma.Common/Models/DTOs/PlatformBanDTO.cs +++ b/WowsKarma.Common/Models/DTOs/PlatformBanDTO.cs @@ -24,10 +24,10 @@ public record PlatformBanDTO [Required] public string Reason { get; set; } = string.Empty; - public DateTime? BannedUntil { get; set; } + public DateTimeOffset? BannedUntil { get; set; } public bool Reverted { get; set; } - public DateTime CreatedAt { get; init; } - public DateTime UpdatedAt { get; set; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; set; } } diff --git a/WowsKarma.Common/Models/DTOs/PlayerClanProfileDTO.cs b/WowsKarma.Common/Models/DTOs/PlayerClanProfileDTO.cs index f915a0ca..9f441a29 100644 --- a/WowsKarma.Common/Models/DTOs/PlayerClanProfileDTO.cs +++ b/WowsKarma.Common/Models/DTOs/PlayerClanProfileDTO.cs @@ -10,5 +10,5 @@ public record PlayerClanProfileDTO // HACK: DateOnly / TimeOnly serialization is not supported by STJ as of now. // See: https://github.com/dotnet/runtime/issues/53539 - public DateTime JoinedClanAt { get; init; } + public DateTimeOffset JoinedClanAt { get; init; } } \ No newline at end of file diff --git a/WowsKarma.Common/Models/DTOs/PlayerPostDTO.cs b/WowsKarma.Common/Models/DTOs/PlayerPostDTO.cs index 107d08c9..87693cdf 100644 --- a/WowsKarma.Common/Models/DTOs/PlayerPostDTO.cs +++ b/WowsKarma.Common/Models/DTOs/PlayerPostDTO.cs @@ -24,6 +24,6 @@ public record PlayerPostDTO public ReplayState ReplayState { get; init; } // Computed by DB Engine (hopefully) - public DateTime? CreatedAt { get; init; } - public DateTime? UpdatedAt { get; init; } + public DateTimeOffset? CreatedAt { get; init; } + public DateTimeOffset? UpdatedAt { get; init; } } diff --git a/WowsKarma.Common/Models/DTOs/PlayerProfileDTO.cs b/WowsKarma.Common/Models/DTOs/PlayerProfileDTO.cs index 49b9fe9b..8bb81e2e 100644 --- a/WowsKarma.Common/Models/DTOs/PlayerProfileDTO.cs +++ b/WowsKarma.Common/Models/DTOs/PlayerProfileDTO.cs @@ -18,9 +18,9 @@ public record PlayerProfileDTO public int RatingTeamplay { get; init; } public int RatingCourtesy { get; init; } - public DateTime WgAccountCreatedAt { get; init; } - public DateTime LastBattleTime { get; init; } - public DateTime OptOutChanged { get; init; } + public DateTimeOffset WgAccountCreatedAt { get; init; } + public DateTimeOffset LastBattleTime { get; init; } + public DateTimeOffset OptOutChanged { get; init; } public bool NegativeKarmaAble { get; init; } public bool PostsBanned { get; init; } diff --git a/WowsKarma.Common/Models/DTOs/UserProfileFlagsDTO.cs b/WowsKarma.Common/Models/DTOs/UserProfileFlagsDTO.cs index 1253ef29..f854b766 100644 --- a/WowsKarma.Common/Models/DTOs/UserProfileFlagsDTO.cs +++ b/WowsKarma.Common/Models/DTOs/UserProfileFlagsDTO.cs @@ -7,7 +7,7 @@ public sealed record UserProfileFlagsDTO public bool PostsBanned { get; init; } public bool OptedOut { get; init; } - public DateTime OptOutChanged { get; init; } + public DateTimeOffset OptOutChanged { get; init; } public IEnumerable ProfileRoles { get; init; } } diff --git a/WowsKarma.Common/Models/INotification.cs b/WowsKarma.Common/Models/INotification.cs index b892a940..7223b76d 100644 --- a/WowsKarma.Common/Models/INotification.cs +++ b/WowsKarma.Common/Models/INotification.cs @@ -8,8 +8,8 @@ public interface INotification public NotificationType Type { get; } - public DateTime EmittedAt { get; } + public DateTimeOffset EmittedAt { get; } - public DateTime? AcknowledgedAt { get; } + public DateTimeOffset? AcknowledgedAt { get; } } } From 10129fea370e5d49f26a85d69fe0d25b90b1c9e6 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 11 Jan 2024 10:04:16 +0100 Subject: [PATCH 09/20] fix(post/edit): Fix various validation issues - In NotificationBase.cs, fixed the initialization of EmittedAt property to use DateTimeOffset.UtcNow instead of DateTime.UtcNow. - In PostModAction.cs, removed redundant empty lines. - In ReplayChatMessageDTO.cs, marked PlayerId, Username, MessageGroup, and MessageContent properties as nullable to disable API validation. - In ReplayDTO.cs, initialized DownloadUri with an empty string and Players and ChatMessages properties with empty arrays. --- .../Data/Models/Notifications/NotificationBase.cs | 2 +- WowsKarma.Api/Data/Models/PostModAction.cs | 2 -- .../Models/DTOs/Replays/ReplayChatMessageDTO.cs | 11 +++++++---- WowsKarma.Common/Models/DTOs/Replays/ReplayDTO.cs | 8 ++++---- WowsKarma.Common/WowsKarma.Common.csproj | 3 ++- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/WowsKarma.Api/Data/Models/Notifications/NotificationBase.cs b/WowsKarma.Api/Data/Models/Notifications/NotificationBase.cs index b24fad67..6052bc49 100644 --- a/WowsKarma.Api/Data/Models/Notifications/NotificationBase.cs +++ b/WowsKarma.Api/Data/Models/Notifications/NotificationBase.cs @@ -14,7 +14,7 @@ public abstract record NotificationBase : INotification public abstract NotificationType Type { get; private protected init; } - public DateTimeOffset EmittedAt { get; private protected init; } = DateTime.UtcNow; + public DateTimeOffset EmittedAt { get; private protected init; } = DateTimeOffset.UtcNow; public DateTimeOffset? AcknowledgedAt { get; set; } public virtual NotificationBaseDTO ToDTO() => new() diff --git a/WowsKarma.Api/Data/Models/PostModAction.cs b/WowsKarma.Api/Data/Models/PostModAction.cs index f1cc5887..e0a5aa6d 100644 --- a/WowsKarma.Api/Data/Models/PostModAction.cs +++ b/WowsKarma.Api/Data/Models/PostModAction.cs @@ -16,8 +16,6 @@ public record PostModAction public Post Post { get; init; } public Guid PostId { get; init; } - - public ModActionType ActionType { get; init; } public Player Mod { get; init; } diff --git a/WowsKarma.Common/Models/DTOs/Replays/ReplayChatMessageDTO.cs b/WowsKarma.Common/Models/DTOs/Replays/ReplayChatMessageDTO.cs index 7829592d..c8c46987 100644 --- a/WowsKarma.Common/Models/DTOs/Replays/ReplayChatMessageDTO.cs +++ b/WowsKarma.Common/Models/DTOs/Replays/ReplayChatMessageDTO.cs @@ -1,11 +1,14 @@ namespace WowsKarma.Common.Models.DTOs.Replays; +/// +/// HACK: Fields are set as nullable to disable API validation. +/// public struct ReplayChatMessageDTO { - public uint PlayerId { get; init; } - public string Username { get; init; } + public uint? PlayerId { get; init; } + public string? Username { get; init; } - public string MessageGroup { get; init; } + public string? MessageGroup { get; init; } - public string MessageContent { get; init; } + public string? MessageContent { get; init; } } diff --git a/WowsKarma.Common/Models/DTOs/Replays/ReplayDTO.cs b/WowsKarma.Common/Models/DTOs/Replays/ReplayDTO.cs index 2e2a19f0..3f368ed8 100644 --- a/WowsKarma.Common/Models/DTOs/Replays/ReplayDTO.cs +++ b/WowsKarma.Common/Models/DTOs/Replays/ReplayDTO.cs @@ -1,16 +1,16 @@ namespace WowsKarma.Common.Models.DTOs.Replays; -public record ReplayDTO +public sealed record ReplayDTO { public Guid Id { get; init; } public Guid PostId { get; init; } - public string DownloadUri { get; init; } + public string DownloadUri { get; init; } = ""; public string? MinimapUri { get; init; } - public IEnumerable Players { get; set; } + public IEnumerable Players { get; set; } = []; - public IEnumerable ChatMessages { get; set; } + public IEnumerable ChatMessages { get; set; } = []; } diff --git a/WowsKarma.Common/WowsKarma.Common.csproj b/WowsKarma.Common/WowsKarma.Common.csproj index ed4d7163..c527277c 100644 --- a/WowsKarma.Common/WowsKarma.Common.csproj +++ b/WowsKarma.Common/WowsKarma.Common.csproj @@ -1,9 +1,10 @@  - net7.0 + net8.0 preview enable + enable From be0a621250f6a836df2a395b6bd65d78bb632bea Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 11 Jan 2024 13:00:57 +0100 Subject: [PATCH 10/20] fix(replays): Fix null ref on empty chat messages list This commit fixes a null reference exception that occurs when the chat messages list is empty. The code now checks if the chat messages list is null before adapting it to an IEnumerable of ReplayChatMessageDTO objects. If the list is null, an empty array is assigned instead. This prevents the null reference exception from occurring. No other significant changes were made in this commit. --- WowsKarma.Api/Services/Replays/ReplaysIngestService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WowsKarma.Api/Services/Replays/ReplaysIngestService.cs b/WowsKarma.Api/Services/Replays/ReplaysIngestService.cs index 88c49d23..dcd30dae 100644 --- a/WowsKarma.Api/Services/Replays/ReplaysIngestService.cs +++ b/WowsKarma.Api/Services/Replays/ReplaysIngestService.cs @@ -59,8 +59,8 @@ public async Task GetReplayDTOAsync(Guid id) { Id = replay.Id, PostId = replay.PostId, - ChatMessages = replay.ChatMessages.Adapt>() - .Select(m => m with { Username = replay.Players.FirstOrDefault(p => p.AccountId == m.PlayerId).Name }), + ChatMessages = replay.ChatMessages?.Adapt>() + .Select(m => m with { Username = replay.Players.FirstOrDefault(p => p.AccountId == m.PlayerId).Name }) ?? [], Players = replay.Players.Adapt>(), DownloadUri = $"{_containerClient.Uri}/{ReplayBlobContainer}/{replay.BlobName}", MinimapUri = replay.MinimapRendered ? $"{_serviceClient.Uri}{MinimapRenderingService.MinimapBlobContainer}/{replay.Id}.mp4" : null From 89de9210b59467d0a0be60f2d73975c68d65fc4a Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 11 Jan 2024 13:02:15 +0100 Subject: [PATCH 11/20] build(deps/minimap-client): Update to .NET 8.0 + Bump package references - Updated JetBrains.Annotations to version [2023.3,) - Updated Microsoft.Extensions.Http to version [8.0.0,) - Updated Microsoft.Extensions.Options.ConfigurationExtensions to version [8.0.0,) - Updated Microsoft.IdentityModel.JsonWebTokens to version [7.2.0,) - Updated System.Net.Http.Json to version [8.0.0,) This commit updates the package references in the WowsKarma.Api.Minimap.Client project file, bumping up the versions of several dependencies for compatibility and potential bug fixes or enhancements. --- .../WowsKarma.Api.Minimap.Client.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/WowsKarma.Api.Minimap.Client/WowsKarma.Api.Minimap.Client.csproj b/WowsKarma.Api.Minimap.Client/WowsKarma.Api.Minimap.Client.csproj index 6ce8979b..a5956b94 100644 --- a/WowsKarma.Api.Minimap.Client/WowsKarma.Api.Minimap.Client.csproj +++ b/WowsKarma.Api.Minimap.Client/WowsKarma.Api.Minimap.Client.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.1 + 0.1.1 WOWS Karma Minimap API Client Sakura Akeno Isayeki @@ -15,11 +15,11 @@ - - - - - + + + + + From 006ff2e0ef4c978aae59b45629129bd38f3ad011 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 11 Jan 2024 13:02:54 +0100 Subject: [PATCH 12/20] build(deps/api): Update dependencies - Updated Azure.Storage.Blobs package version to 12.20.0-beta.1 - Updated DSharpPlus package version to 4.4.6 - Added ExpressionDebugger package with version 2.2.1 - Updated Microsoft.AspNetCore.Authentication.JwtBearer package version to 8.0.1 --- WowsKarma.Api/WowsKarma.Api.csproj | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/WowsKarma.Api/WowsKarma.Api.csproj b/WowsKarma.Api/WowsKarma.Api.csproj index 0247aade..d3494a60 100644 --- a/WowsKarma.Api/WowsKarma.Api.csproj +++ b/WowsKarma.Api/WowsKarma.Api.csproj @@ -3,6 +3,8 @@ net8.0 preview + enable + 0.17.2 0.17.2 Sakura Akeno Isayeki @@ -23,8 +25,9 @@ - + + @@ -34,7 +37,7 @@ - + From daadec2e214e1d921cd782eb074e5c5b4a9bf2b9 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Fri, 12 Jan 2024 12:45:34 +0100 Subject: [PATCH 13/20] feat!: The great nullification - Adapted the code to nullable context - Sealed all remaining classes - Refactored various parts of the code to match new NET 8.0 standards --- .../Controllers/Admin/ModActionController.cs | 17 +++--- .../Admin/PlatformBansController.cs | 23 +++---- WowsKarma.Api/Controllers/AuthController.cs | 43 ++++++------- WowsKarma.Api/Controllers/ClanController.cs | 17 +++--- WowsKarma.Api/Controllers/PlayerController.cs | 6 +- WowsKarma.Api/Controllers/PostController.cs | 61 +++++++++---------- .../Controllers/ProfileController.cs | 6 +- WowsKarma.Api/Controllers/ReplayController.cs | 16 ++--- WowsKarma.Api/Controllers/StatusController.cs | 7 +-- WowsKarma.Api/Data/ApiDbContext.cs | 32 +++++----- WowsKarma.Api/Data/AuthDbContext.cs | 5 +- WowsKarma.Api/Data/Models/Auth/Role.cs | 8 +-- WowsKarma.Api/Data/Models/Auth/User.cs | 5 +- WowsKarma.Api/Data/Models/Clan.cs | 6 +- WowsKarma.Api/Data/Models/ClanMember.cs | 14 ++--- WowsKarma.Api/Data/Models/PlatformBan.cs | 8 +-- WowsKarma.Api/Data/Models/Player.cs | 14 ++--- WowsKarma.Api/Data/Models/Post.cs | 12 ++-- WowsKarma.Api/Data/Models/PostModAction.cs | 10 ++- WowsKarma.Api/Data/Models/Replays/Replay.cs | 13 ++-- .../Data/Models/Replays/ReplayArenaInfo.cs | 17 +++--- .../Data/Models/Replays/ReplayPlayer.cs | 4 +- WowsKarma.Api/Hubs/AuthHub.cs | 2 +- WowsKarma.Api/Hubs/NotificationsHub.cs | 13 ++-- WowsKarma.Api/Hubs/PostHub.cs | 2 +- .../Attributes/ETagAttribute.cs | 2 +- .../HangfireDashboardAuthorizationFilter.cs | 3 +- .../PlatformBanAuthorizationHandler.cs | 5 +- .../Authorization/PlatformBanRequirement.cs | 2 +- WowsKarma.Api/Infrastructure/Data/Page.cs | 2 - .../Telemetry/HubTelemetryFilter.cs | 4 +- .../Telemetry/TelemetryEnrichment.cs | 7 +-- WowsKarma.Api/Middlewares/ETagMiddleware.cs | 7 +-- .../Middlewares/RequestLoggingMiddleware.cs | 25 ++++---- .../ApiDb/20220311183423_AddClans.cs | 3 +- ...20312144159_ReplaceClanMemberNavigation.cs | 3 +- ...20220730123556_AddModEditedNotification.cs | 3 +- .../20220309215406_UpdateHeuristicsFormat.cs | 4 +- WowsKarma.Api/Program.cs | 7 +-- .../ForwardCookieAuthenticationHandler.cs | 14 ++--- .../Services/Authentication/Jwt/ApiRole.cs | 6 +- .../Authentication/Jwt/ApplicationUser.cs | 4 +- .../Jwt/JwtAuthenticationHandler.cs | 6 +- .../Services/Authentication/Jwt/JwtService.cs | 26 ++++---- .../Services/Authentication/UserService.cs | 42 ++++++++----- .../Wargaming/WargamingAuthClientFactory.cs | 11 ++-- .../Wargaming/WargamingAuthExtensions.cs | 11 ++-- .../Wargaming/WargamingAuthService.cs | 51 +++++++--------- .../Wargaming/WargamingIdentity.cs | 15 ++--- WowsKarma.Api/Services/ClanService.cs | 27 ++++---- .../Discord/ModActionWebhookService.cs | 12 ++-- .../Services/Discord/PostWebhookService.cs | 9 ++- .../Services/Discord/WebhookService.cs | 18 +++--- WowsKarma.Api/Services/KarmaService.cs | 4 +- .../Services/MinimapRenderingService.cs | 8 +-- WowsKarma.Api/Services/ModService.cs | 25 +++++--- WowsKarma.Api/Services/NotificationService.cs | 26 ++++---- WowsKarma.Api/Services/PlayerService.cs | 7 +-- WowsKarma.Api/Services/Posts/PostService.cs | 51 +++++++++------- .../Posts/PostUpdatesBroadcastService.cs | 16 +++-- .../Services/Replays/ReplaysIngestService.cs | 45 ++++++++------ .../Services/Replays/ReplaysProcessService.cs | 7 +-- WowsKarma.Api/Startup.cs | 6 +- WowsKarma.Api/Utilities/Conversions.cs | 9 ++- WowsKarma.Api/Utilities/HttpExtensions.cs | 9 +-- WowsKarma.Api/Utilities/Links.cs | 18 +++--- WowsKarma.Api/Utilities/LinqExtensions.cs | 3 +- WowsKarma.Api/Utilities/Reflection.cs | 5 +- WowsKarma.Api/WowsKarma.Api.csproj | 1 + .../Models/DTOs/PlayerProfileDTO.cs | 2 +- WowsKarma.Common/Models/PostFlairs.cs | 7 ++- WowsKarma.Common/Utilities.cs | 27 +++++--- 72 files changed, 467 insertions(+), 499 deletions(-) diff --git a/WowsKarma.Api/Controllers/Admin/ModActionController.cs b/WowsKarma.Api/Controllers/Admin/ModActionController.cs index f98ad40e..88f4b371 100644 --- a/WowsKarma.Api/Controllers/Admin/ModActionController.cs +++ b/WowsKarma.Api/Controllers/Admin/ModActionController.cs @@ -6,12 +6,11 @@ using WowsKarma.Api.Services.Posts; using WowsKarma.Common; - namespace WowsKarma.Api.Controllers.Admin; [ApiController, Route("api/mod/action"), Authorize(Roles = ApiRoles.CM)] -public class ModActionController : ControllerBase +public sealed class ModActionController : ControllerBase { private readonly ModService _service; @@ -40,22 +39,22 @@ public ModActionController(ModService service) [HttpGet("list"), AllowAnonymous, ProducesResponseType(typeof(IEnumerable), 200), ProducesResponseType(204)] public IActionResult List([FromQuery] Guid postId = default, [FromQuery] uint userId = default) { - IEnumerable modActions; + PostModAction[] modActions = []; if (postId != default) { - modActions = _service.GetPostModActions(postId).ToArray(); + modActions = [.. _service.GetPostModActions(postId)]; } else if (userId is not 0) { - modActions = _service.GetPostModActions(userId).ToArray(); + modActions = [.. _service.GetPostModActions(userId)]; } else { return BadRequest("Please use a search query (Post/User)."); } - return modActions?.Count() is null or 0 + return modActions is [] ? base.StatusCode(204) : base.StatusCode(200, modActions.Adapt>()); } @@ -73,8 +72,8 @@ public IActionResult List([FromQuery] Guid postId = default, [FromQuery] uint us public async Task Submit([FromBody] PostModActionDTO modAction, [FromServices] PostService postService) { - Post post = postService.GetPost(modAction.PostId); - uint modId = uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)); + Post post = postService.GetPost(modAction.PostId) ?? throw new InvalidOperationException("Post ID is invalid."); + uint modId = uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new BadHttpRequestException("Missing NameIdentifier claim.")); if ((post.AuthorId == modId || post.PlayerId == modId) && !User.IsInRole(ApiRoles.Administrator)) { @@ -82,7 +81,7 @@ public async Task Submit([FromBody] PostModActionDTO modAction, } await _service.SubmitPostModActionAsync(modAction with { ModId = modId }); - return StatusCode(202); + return Accepted(); } /// diff --git a/WowsKarma.Api/Controllers/Admin/PlatformBansController.cs b/WowsKarma.Api/Controllers/Admin/PlatformBansController.cs index 7159c7ed..a0721cb2 100644 --- a/WowsKarma.Api/Controllers/Admin/PlatformBansController.cs +++ b/WowsKarma.Api/Controllers/Admin/PlatformBansController.cs @@ -9,7 +9,7 @@ namespace WowsKarma.Api.Controllers.Admin; [ApiController, Route("api/mod/bans"), Authorize(Roles = $"{ApiRoles.CM},{ApiRoles.Administrator}")] -public class PlatformBansController : ControllerBase +public sealed class PlatformBansController : ControllerBase { private readonly ModService _service; @@ -39,23 +39,24 @@ public IActionResult FetchBans(uint userId, bool currentOnly) return Ok(bans.ProjectToType().AsAsyncEnumerable()); } - /// - /// Emits a new Platform Ban. - /// - /// Platform Ban to emit - /// (Helper) Sets a temporary ban, to the number of specified days starting from UTC now. - /// Platform Ban was successfuly submitted. + /// + /// Emits a new Platform Ban. + /// + /// Platform Ban to emit + /// (DI) + /// (Helper) Sets a temporary ban, to the number of specified days starting from UTC now. + /// Platform Ban was successfuly submitted. [HttpPost, ProducesResponseType(202)] public async Task SubmitBan([FromBody] PlatformBanDTO submitted, [FromServices] AuthDbContext authDb, [FromQuery] uint days = 0) { await _service.EmitPlatformBanAsync(submitted with { - ModId = User.ToAccountListing().Id, + ModId = User.ToAccountListing()!.Id, Reverted = false, - BannedUntil = days is 0 ? null : DateTime.UtcNow.AddDays(days) + BannedUntil = days is 0 ? null : DateTimeOffset.UtcNow.AddDays(days) }, authDb); - return StatusCode(202); + return Accepted(); } /// @@ -64,7 +65,7 @@ await _service.EmitPlatformBanAsync(submitted with /// ID of Platform Ban to revert. /// Platform Ban was successfully reverted. [HttpDelete("{id:guid}"), ProducesResponseType(200)] - public async Task RevertBan([FromQuery] Guid id) + public async Task RevertBan(Guid id) { await _service.RevertPlatformBanAsync(id); diff --git a/WowsKarma.Api/Controllers/AuthController.cs b/WowsKarma.Api/Controllers/AuthController.cs index 766a6e92..9bc28a8e 100644 --- a/WowsKarma.Api/Controllers/AuthController.cs +++ b/WowsKarma.Api/Controllers/AuthController.cs @@ -2,35 +2,32 @@ using Microsoft.AspNetCore.Mvc; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; -using Microsoft.AspNetCore.Http; using WowsKarma.Api.Infrastructure.Attributes; using WowsKarma.Api.Services.Authentication; using WowsKarma.Api.Services.Authentication.Jwt; using WowsKarma.Api.Services.Authentication.Wargaming; using WowsKarma.Common; - namespace WowsKarma.Api.Controllers; - /// /// Provides API Authentication endpoints. /// [ApiController, Route("api/[controller]"), ETag(false)] -public class AuthController : ControllerBase +public sealed class AuthController : ControllerBase { - private readonly IConfiguration config; - private readonly UserService userService; - private readonly WargamingAuthService wargamingAuthService; - private readonly JwtService jwtService; + private readonly IConfiguration _config; + private readonly UserService _userService; + private readonly WargamingAuthService _wargamingAuthService; + private readonly JwtService _jwtService; public AuthController(IConfiguration config, UserService userService, WargamingAuthService wargamingAuthService, JwtService jwtService) { - this.config = config; - this.userService = userService; - this.wargamingAuthService = wargamingAuthService; - this.jwtService = jwtService; + _config = config; + _userService = userService; + _wargamingAuthService = wargamingAuthService; + _jwtService = jwtService; } /// @@ -57,33 +54,33 @@ public AuthController(IConfiguration config, UserService userService, WargamingA [HttpGet("wg-callback"), ProducesResponseType(302), ProducesResponseType(200), ProducesResponseType(403)] public async Task WgAuthCallback() { - bool valid = await wargamingAuthService.VerifyIdentity(Request); + bool valid = await _wargamingAuthService.VerifyIdentity(Request); if (!valid) { return StatusCode(403); } - JwtSecurityToken token = await userService.CreateTokenAsync(WargamingIdentity.FromUri(new(Request.Query["openid.identity"].FirstOrDefault() + JwtSecurityToken token = await _userService.CreateTokenAsync(WargamingIdentity.FromUri(new(Request.Query["openid.identity"].FirstOrDefault() ?? throw new BadHttpRequestException("Missing OpenID identity")))); Response.Cookies.Append( - config[$"Api:{Startup.ApiRegion.ToRegionString()}:CookieName"], - jwtService.TokenHandler.WriteToken(token), + _config[$"Api:{Startup.ApiRegion.ToRegionString()}:CookieName"] ?? throw new ApplicationException("Missing Api:{region}:CookieName in configuration."), + _jwtService.TokenHandler.WriteToken(token), new() { - Domain = config[$"Api:{Startup.ApiRegion.ToRegionString()}:CookieDomain"], + Domain = _config[$"Api:{Startup.ApiRegion.ToRegionString()}:CookieDomain"], HttpOnly = false, IsEssential = true, #if RELEASE - Secure = true, + Secure = true, #endif Expires = DateTime.UtcNow.AddDays(7) }); return Request.Query["redirectUri"].FirstOrDefault() is { } redirectUri ? Redirect(redirectUri) - : StatusCode(200); + : Ok(); } /// @@ -94,8 +91,8 @@ public async Task WgAuthCallback() [HttpPost("renew-seed"), Authorize, ProducesResponseType(200), ProducesResponseType(401)] public async Task RenewSeed() { - await userService.RenewSeedTokenAsync(uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier))); - return StatusCode(200); + await _userService.RenewSeedTokenAsync(uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new BadHttpRequestException("Missing NameIdentifier claim."))); + return Ok(); } /// @@ -106,7 +103,7 @@ public async Task RenewSeed() [HttpGet("refresh-token"), Authorize, ProducesResponseType(typeof(string), 200), ProducesResponseType(401)] public async Task RefreshToken() { - JwtSecurityToken token = await userService.CreateTokenAsync(new(User.Claims)); - return StatusCode(200, jwtService.TokenHandler.WriteToken(token)); + JwtSecurityToken token = await _userService.CreateTokenAsync(new(User.Claims)); + return StatusCode(200, _jwtService.TokenHandler.WriteToken(token)); } } diff --git a/WowsKarma.Api/Controllers/ClanController.cs b/WowsKarma.Api/Controllers/ClanController.cs index 442b6a11..9bdbf6c9 100644 --- a/WowsKarma.Api/Controllers/ClanController.cs +++ b/WowsKarma.Api/Controllers/ClanController.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using System.Threading; using Mapster; using Microsoft.AspNetCore.Mvc; using WowsKarma.Api.Services; @@ -8,7 +7,7 @@ namespace WowsKarma.Api.Controllers; [ApiController, Route("api/[controller]")] -public class ClanController : ControllerBase +public sealed class ClanController : ControllerBase { private readonly ClanService _clanService; @@ -34,14 +33,12 @@ public ClanController(ClanService clanService) /// /// Clan Info, with members (if selected) [HttpGet("{clanId}"), ProducesResponseType(typeof(ClanProfileDTO), 200), ProducesResponseType(typeof(ClanProfileFullDTO), 200)] - public async Task GetClan(uint clanId, bool includeMembers = true, CancellationToken ct = default) - { - Clan clan = await _clanService.GetClanAsync(clanId, includeMembers, ct); - - return includeMembers - ? clan.Adapt() - : clan.Adapt(); - } + public async Task GetClan(uint clanId, bool includeMembers = true, CancellationToken ct = default) + => await _clanService.GetClanAsync(clanId, includeMembers, ct) is { } clan + ? includeMembers + ? clan.Adapt() + : clan.Adapt() + : null; /// /// Searches all clans relevant to a given search string. diff --git a/WowsKarma.Api/Controllers/PlayerController.cs b/WowsKarma.Api/Controllers/PlayerController.cs index 328f3dc7..89315662 100644 --- a/WowsKarma.Api/Controllers/PlayerController.cs +++ b/WowsKarma.Api/Controllers/PlayerController.cs @@ -1,17 +1,15 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; -using System.Threading; using Hangfire; using Mapster; using WowsKarma.Api.Services; using WowsKarma.Common; - namespace WowsKarma.Api.Controllers; [ApiController, Route("api/[controller]")] -public class PlayerController : ControllerBase +public sealed class PlayerController : ControllerBase { private readonly PlayerService _playerService; @@ -63,7 +61,7 @@ public async Task GetAccount(uint id, bool includeClanInfo = true Player playerProfile = await _playerService.GetPlayerAsync(id, false, includeClanInfo); return playerProfile is null - ? NoContent() + ? NotFound() : Ok(playerProfile.Adapt()); } diff --git a/WowsKarma.Api/Controllers/PostController.cs b/WowsKarma.Api/Controllers/PostController.cs index 5126d5ba..a2e601f2 100644 --- a/WowsKarma.Api/Controllers/PostController.cs +++ b/WowsKarma.Api/Controllers/PostController.cs @@ -4,10 +4,7 @@ using Microsoft.AspNetCore.Mvc; using System.Security.Claims; using System.Text.Json; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using WowsKarma.Api.Data.Models.Replays; using WowsKarma.Api.Infrastructure.Attributes; using WowsKarma.Api.Infrastructure.Data; using WowsKarma.Api.Services; @@ -22,14 +19,14 @@ namespace WowsKarma.Api.Controllers; [ApiController, Route("api/[controller]")] public sealed class PostController : ControllerBase { - private readonly PlayerService playerService; - private readonly PostService postService; + private readonly PlayerService _playerService; + private readonly PostService _postService; private readonly ILogger _logger; public PostController(PlayerService playerService, PostService postService, ILogger logger) { - this.playerService = playerService ?? throw new ArgumentNullException(nameof(playerService)); - this.postService = postService ?? throw new ArgumentNullException(nameof(postService)); + _playerService = playerService ?? throw new ArgumentNullException(nameof(playerService)); + _postService = postService ?? throw new ArgumentNullException(nameof(postService)); _logger = logger; } @@ -38,7 +35,7 @@ public PostController(PlayerService playerService, PostService postService, ILog /// /// List of post IDs. [HttpGet, ProducesResponseType(StatusCodes.Status200OK)] - public IAsyncEnumerable GetPostIds() => postService.ListPostIdsAsync(); + public IAsyncEnumerable GetPostIds() => _postService.ListPostIdsAsync(); /// /// Fetches player post with given ID @@ -49,8 +46,8 @@ public PostController(PlayerService playerService, PostService postService, ILog /// Post is locked by Community Managers. [HttpGet("{postId:guid}"), ProducesResponseType(typeof(PlayerPostDTO), 200), ProducesResponseType(404), ProducesResponseType(410)] public async Task GetPostAsync(Guid postId) - => await postService.GetPostDTOAsync(postId) is { } post - ? !post.ModLocked || post.Author.Id == User.ToAccountListing().Id || User.IsInRole(ApiRoles.CM) + => await _postService.GetPostDTOAsync(postId) is { } post + ? !post.ModLocked || post.Author.Id == User.ToAccountListing()?.Id || User.IsInRole(ApiRoles.CM) ? Ok(post) : StatusCode(410) : NotFound(); @@ -69,11 +66,11 @@ public IActionResult GetReceivedPosts( [FromQuery] int page = 1, [FromQuery] int pageSize = 10 ) { - IQueryable posts = postService.GetReceivedPosts(userId); + IQueryable posts = _postService.GetReceivedPosts(userId); if (User.ToAccountListing()?.Id != userId || !User.IsInRole(ApiRoles.CM)) { - AccountListingDTO currentUser = User.ToAccountListing(); + AccountListingDTO? currentUser = User.ToAccountListing(); posts = posts.Where(p => !p.ModLocked || (currentUser != null && p.AuthorId == currentUser.Id)); } @@ -110,14 +107,14 @@ public IActionResult GetSentPosts( [FromQuery] int page = 1, [FromQuery] int pageSize = 10 ) { - IQueryable posts = postService.GetSentPosts(userId); + IQueryable posts = _postService.GetSentPosts(userId); if (User.ToAccountListing()?.Id != userId || !User.IsInRole(ApiRoles.CM)) { - posts = posts?.Where(static p => !p.ModLocked); + posts = posts.Where(static p => !p.ModLocked); } - posts?.Include(static p => p.Replay); + posts.Include(static p => p.Replay); // Get the page of results and set headers Page pageResults = posts.Page(pageSize, page); @@ -150,9 +147,9 @@ public IActionResult GetLatestPosts( [FromQuery] bool? hasReplay = null, [FromQuery] bool hideModActions = false ) { - AccountListingDTO currentUser = User.ToAccountListing(); + AccountListingDTO? currentUser = User.ToAccountListing(); - IQueryable posts = postService.GetLatestPosts(); + IQueryable posts = _postService.GetLatestPosts(); if (!User.IsInRole(ApiRoles.CM)) { @@ -191,7 +188,7 @@ public IActionResult GetLatestPosts( /// /// Submits a new post for creation. /// - /// Post object to submit + /// Post object to submit /// Optional replay file to attach to post /// Bypass API Validation for post creation (Admin only) /// Post was successfuly created. @@ -204,26 +201,26 @@ public IActionResult GetLatestPosts( public async Task CreatePost( [FromForm] string postDto, [FromServices] ReplaysIngestService replaysIngestService, - IFormFile replay = null, + IFormFile? replay = null, [FromQuery] bool ignoreChecks = false) { PlayerPostDTO post; try { - post = JsonSerializer.Deserialize(postDto, Common.Utilities.ApiSerializerOptions); + post = JsonSerializer.Deserialize(postDto, Common.Utilities.ApiSerializerOptions) ?? throw new ArgumentNullException(nameof(postDto)); } catch (Exception e) { return BadRequest(e.ToString()); } - if (await playerService.GetPlayerAsync(post.Author.Id) is not { } author) + if (await _playerService.GetPlayerAsync(post.Author.Id) is not { } author) { return StatusCode(404, $"Account {post.Author.Id} not found."); } - if (await playerService.GetPlayerAsync(post.Player.Id) is not { } player) + if (await _playerService.GetPlayerAsync(post.Player.Id) is not { } player) { return StatusCode(404, $"Account {post.Player.Id} not found."); } @@ -237,7 +234,7 @@ public async Task CreatePost( } else { - if (post.Author.Id != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier))) + if (post.Author.Id != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new BadHttpRequestException("Missing NameIdentifier claim."))) { return StatusCode(403, "Author is not authorized to post on behalf of other users."); } @@ -255,7 +252,7 @@ public async Task CreatePost( try { - Post created = await postService.CreatePostAsync(post, replay, ignoreChecks); + Post created = await _postService.CreatePostAsync(post, replay, ignoreChecks); return StatusCode(201, created.Id); } catch (ArgumentException) @@ -272,7 +269,7 @@ public async Task CreatePost( { // Log this exception, and store the replay with the RCE the samples. _logger.LogWarning(se, "Replay upload failed for post author {author} due to CVE-2022-31265 exploit detection.", post.Author.Id); - await replaysIngestService.IngestRceFileAsync(replay); + await replaysIngestService.IngestRceFileAsync(replay!); throw se; } @@ -291,7 +288,7 @@ public async Task CreatePost( [ProducesResponseType(200), ProducesResponseType(400), ProducesResponseType(typeof(string), 403), ProducesResponseType(typeof(string), 404)] public async Task EditPost([FromBody] PlayerPostDTO post, [FromQuery] bool ignoreChecks = false) { - if (postService.GetPost(post.Id ?? Guid.Empty) is not { } current) + if (_postService.GetPost(post.Id ?? Guid.Empty) is not { } current) { return StatusCode(404, $"No post with ID {post.Id} found."); } @@ -305,7 +302,7 @@ public async Task EditPost([FromBody] PlayerPostDTO post, [FromQu } else { - if (current.AuthorId != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier))) + if (current.AuthorId != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new BadHttpRequestException("Missing NameIdentifier claim."))) { return StatusCode(403, "Author is not authorized to edit posts on behalf of other users."); } @@ -318,8 +315,8 @@ public async Task EditPost([FromBody] PlayerPostDTO post, [FromQu try { - await postService.EditPostAsync(post.Id ?? Guid.Empty, post); - return StatusCode(200); + await _postService.EditPostAsync(post.Id ?? Guid.Empty, post); + return Ok(); } catch (ArgumentException e) { @@ -339,7 +336,7 @@ public async Task EditPost([FromBody] PlayerPostDTO post, [FromQu [ProducesResponseType(205), ProducesResponseType(typeof(string), 403), ProducesResponseType(typeof(string), 404)] public async Task DeletePost(Guid postId, [FromQuery] bool ignoreChecks = false) { - if (postService.GetPost(postId) is not { } post) + if (_postService.GetPost(postId) is not { } post) { return StatusCode(404, $"No post with ID {postId} found."); } @@ -353,7 +350,7 @@ public async Task DeletePost(Guid postId, [FromQuery] bool ignore } else { - if (post.AuthorId != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier))) + if (post.AuthorId != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new BadHttpRequestException("Missing NameIdentifier claim."))) { return StatusCode(403, "Author is not authorized to delete posts on behalf of other users."); } @@ -363,7 +360,7 @@ public async Task DeletePost(Guid postId, [FromQuery] bool ignore } } - await postService.DeletePostAsync(postId); + await _postService.DeletePostAsync(postId); return StatusCode(205); } } \ No newline at end of file diff --git a/WowsKarma.Api/Controllers/ProfileController.cs b/WowsKarma.Api/Controllers/ProfileController.cs index 58e54f13..288a7678 100644 --- a/WowsKarma.Api/Controllers/ProfileController.cs +++ b/WowsKarma.Api/Controllers/ProfileController.cs @@ -54,13 +54,13 @@ public async Task UpdateProfileFlagsAsync([FromBody] UserProfileF { try { - if (flags.Id != User.ToAccountListing().Id && !User.IsInRole(ApiRoles.Administrator)) + if (flags.Id != User.ToAccountListing()!.Id && !User.IsInRole(ApiRoles.Administrator)) { return StatusCode(403, "User can only update their own profile."); } await _playerService.UpdateProfileFlagsAsync(flags); - return StatusCode(200); + return Ok(); } catch (CooldownException e) { @@ -68,7 +68,7 @@ public async Task UpdateProfileFlagsAsync([FromBody] UserProfileF } catch (ArgumentException) { - return StatusCode(404); + return NotFound(); } } } \ No newline at end of file diff --git a/WowsKarma.Api/Controllers/ReplayController.cs b/WowsKarma.Api/Controllers/ReplayController.cs index 505f8c80..70bebfc9 100644 --- a/WowsKarma.Api/Controllers/ReplayController.cs +++ b/WowsKarma.Api/Controllers/ReplayController.cs @@ -1,11 +1,8 @@ using System.Security; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; -using System.Threading; using Hangfire; -using Microsoft.Extensions.Logging; using WowsKarma.Api.Data.Models.Replays; using WowsKarma.Api.Infrastructure.Exceptions; using WowsKarma.Api.Services; @@ -16,7 +13,6 @@ namespace WowsKarma.Api.Controllers; - [ApiController, Route("api/[controller]")] public sealed class ReplayController : ControllerBase { @@ -51,7 +47,7 @@ ILogger logger /// ID of Replay to fetch /// Replay data [HttpGet("{replayId:guid}"), ProducesResponseType(typeof(ReplayDTO), 200)] - public Task GetAsync(Guid replayId) => _ingestService.GetReplayDTOAsync(replayId); + public Task GetAsync(Guid replayId) => _ingestService.GetReplayDTOAsync(replayId); [HttpPost("{postId:guid}"), Authorize, RequestSizeLimit(ReplaysIngestService.MaxReplaySize), ProducesResponseType(201)] public async Task UploadReplayAsync(Guid postId, IFormFile replay, CancellationToken ct, [FromQuery] bool ignoreChecks = false) @@ -70,7 +66,7 @@ public async Task UploadReplayAsync(Guid postId, IFormFile replay } else { - if (current.AuthorId != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier))) + if (current.AuthorId != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new InvalidOperationException("Missing NameIdentifier claim."))) { return StatusCode(403, "Post Author is not authorized to edit post replays on behalf of other users."); } @@ -80,7 +76,6 @@ public async Task UploadReplayAsync(Guid postId, IFormFile replay } } - try { Replay ingested = await _ingestService.IngestReplayAsync(postId, replay, ct); @@ -106,7 +101,7 @@ public async Task UploadReplayAsync(Guid postId, IFormFile replay /// Start of date/time range /// End of date/time range [HttpPatch("reprocess/replay/all"), Authorize(Roles = ApiRoles.Administrator)] - public async Task ReprocessPostsAsync(DateTime start = default, DateTime end = default, CancellationToken ct = default) + public IActionResult ReprocessPosts(DateTime start = default, DateTime end = default, CancellationToken ct = default) { if (start == default) { @@ -127,7 +122,7 @@ public async Task ReprocessPostsAsync(DateTime start = default, D /// /// [HttpPatch("reprocess/replay/{replayId:guid}"), Authorize(Roles = ApiRoles.Administrator)] - public async Task ReprocessReplayAsync(Guid replayId, CancellationToken ct = default) + public IActionResult ReprocessReplay(Guid replayId, CancellationToken ct = default) { try { @@ -144,7 +139,6 @@ public async Task ReprocessReplayAsync(Guid replayId, Cancellatio /// Triggers minimap rendering on a post's replay (Usable only by Administrators) /// /// The ID of the post to render the replay's minimap for. - /// /// /// Whether to force rendering the minimap, even if it has already been rendered. /// Whether to wait for the job to complete before returning. @@ -185,7 +179,7 @@ public async ValueTask RenderMinimap(Guid postId, /// Cancellation token /// The job was enqueued successfully. [HttpPatch("reprocess/minimap/all"), Authorize(Roles = ApiRoles.Administrator)] - public async Task RenderMinimapsAsync(DateTime start = default, DateTime end = default, bool force = false, CancellationToken ct = default) + public IActionResult RenderMinimaps(DateTime start = default, DateTime end = default, bool force = false, CancellationToken ct = default) { if (start == default) { diff --git a/WowsKarma.Api/Controllers/StatusController.cs b/WowsKarma.Api/Controllers/StatusController.cs index 318b2210..0cb983b7 100644 --- a/WowsKarma.Api/Controllers/StatusController.cs +++ b/WowsKarma.Api/Controllers/StatusController.cs @@ -1,8 +1,5 @@ using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; using WowsKarma.Api.Infrastructure.Attributes; namespace WowsKarma.Api.Controllers; @@ -11,7 +8,7 @@ namespace WowsKarma.Api.Controllers; /// Provides status endpoints for controlling API lifetime. /// [ApiController, Route("api/[controller]"), ETag(false)] -public class StatusController : Controller +public sealed class StatusController : Controller { /// /// Provides a HTTP ping endpoint. @@ -34,8 +31,6 @@ public IActionResult HandleError() statusCode: StatusCodes.Status500InternalServerError, type: exceptionHandlerFeature.Error.GetType().ToString() ); - - } return Problem(statusCode: StatusCodes.Status500InternalServerError); diff --git a/WowsKarma.Api/Data/ApiDbContext.cs b/WowsKarma.Api/Data/ApiDbContext.cs index ff80ecdb..a1c8f1ee 100644 --- a/WowsKarma.Api/Data/ApiDbContext.cs +++ b/WowsKarma.Api/Data/ApiDbContext.cs @@ -5,29 +5,27 @@ using WowsKarma.Api.Data.Models.Notifications; using WowsKarma.Api.Utilities; - namespace WowsKarma.Api.Data; - public sealed class ApiDbContext : DbContext { - public DbSet Clans { get; init; } - public DbSet ClanMembers { get; init; } - public DbSet PlatformBans { get; init; } - public DbSet Players { get; init; } - public DbSet Posts { get; init; } - public DbSet PostModActions { get; init; } - public DbSet Replays { get; init; } + public DbSet Clans { get; init; } = null!; + public DbSet ClanMembers { get; init; } = null!; + public DbSet PlatformBans { get; init; } = null!; + public DbSet Players { get; init; } = null!; + public DbSet Posts { get; init; } = null!; + public DbSet PostModActions { get; init; } = null!; + public DbSet Replays { get; init; } = null!; #region Notifications - public DbSet Notifications { get; init; } - - public DbSet PlatformBanNotifications { get; init; } - public DbSet PostAddedNotifications { get; init; } - public DbSet PostEditedNotifications { get; init; } - public DbSet PostDeletedNotifications { get; init; } - public DbSet PostModEditedNotifications { get; init; } - public DbSet PostModDeletedNotifications { get; init; } + public DbSet Notifications { get; init; } = null!; + + public DbSet PlatformBanNotifications { get; init; } = null!; + public DbSet PostAddedNotifications { get; init; } = null!; + public DbSet PostEditedNotifications { get; init; } = null!; + public DbSet PostDeletedNotifications { get; init; } = null!; + public DbSet PostModEditedNotifications { get; init; } = null!; + public DbSet PostModDeletedNotifications { get; init; } = null!; #endregion public ApiDbContext(DbContextOptions options) : base(options) diff --git a/WowsKarma.Api/Data/AuthDbContext.cs b/WowsKarma.Api/Data/AuthDbContext.cs index 83cdfdf2..87d9107b 100644 --- a/WowsKarma.Api/Data/AuthDbContext.cs +++ b/WowsKarma.Api/Data/AuthDbContext.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Npgsql; using WowsKarma.Api.Data.Models.Auth; using WowsKarma.Common; @@ -7,8 +6,8 @@ namespace WowsKarma.Api.Data; public class AuthDbContext : DbContext { - public DbSet Users { get; init; } - public DbSet Roles { get; init; } + public DbSet Users { get; init; } = null!; + public DbSet Roles { get; init; } = null!; public AuthDbContext(DbContextOptions options) : base(options) { } diff --git a/WowsKarma.Api/Data/Models/Auth/Role.cs b/WowsKarma.Api/Data/Models/Auth/Role.cs index 3ccf3b6a..a95ba30f 100644 --- a/WowsKarma.Api/Data/Models/Auth/Role.cs +++ b/WowsKarma.Api/Data/Models/Auth/Role.cs @@ -3,16 +3,16 @@ namespace WowsKarma.Api.Data.Models.Auth; -public record Role +public sealed record Role { [Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public byte Id { get; init; } [Required] - public string InternalName { get; init; } + public string InternalName { get; init; } = ""; [Required] - public string DisplayName { get; set; } + public string DisplayName { get; set; } = ""; - public IEnumerable Users { get; set; } + public IEnumerable Users { get; set; } = []; } \ No newline at end of file diff --git a/WowsKarma.Api/Data/Models/Auth/User.cs b/WowsKarma.Api/Data/Models/Auth/User.cs index 02ce9837..4a3df1eb 100644 --- a/WowsKarma.Api/Data/Models/Auth/User.cs +++ b/WowsKarma.Api/Data/Models/Auth/User.cs @@ -1,15 +1,14 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; - namespace WowsKarma.Api.Data.Models.Auth; -public record User +public sealed record User { [Required, DatabaseGenerated(DatabaseGeneratedOption.None)] public uint Id { get; init; } - public List Roles { get; set; } + public List Roles { get; set; } = []; [Required] public Guid SeedToken { get; set; } diff --git a/WowsKarma.Api/Data/Models/Clan.cs b/WowsKarma.Api/Data/Models/Clan.cs index 3ab9f8df..d048ccea 100644 --- a/WowsKarma.Api/Data/Models/Clan.cs +++ b/WowsKarma.Api/Data/Models/Clan.cs @@ -8,10 +8,10 @@ public sealed record Clan : ITimestamped [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] public uint Id { get; init; } - public string Tag { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; + public string Tag { get; set; } = ""; + public string Name { get; set; } = ""; - public string Description { get; set; } = string.Empty; + public string Description { get; set; } = ""; public uint LeagueColor { get; set; } diff --git a/WowsKarma.Api/Data/Models/ClanMember.cs b/WowsKarma.Api/Data/Models/ClanMember.cs index 6dafb302..0d17c66f 100644 --- a/WowsKarma.Api/Data/Models/ClanMember.cs +++ b/WowsKarma.Api/Data/Models/ClanMember.cs @@ -1,28 +1,26 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations.Schema; using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows; namespace WowsKarma.Api.Data.Models; -public record ClanMember : IComparable, IComparable +public sealed record ClanMember : IComparable, IComparable { [DatabaseGenerated(DatabaseGeneratedOption.None)] public uint PlayerId { get; init; } - public virtual Player Player { get; init; } + public Player Player { get; init; } = null!; [DatabaseGenerated(DatabaseGeneratedOption.None)] public uint ClanId { get; init; } - public virtual Clan Clan { get; init; } + public Clan Clan { get; init; } = null!; public DateOnly JoinedAt { get; init; } public DateOnly? LeftAt { get; set; } public ClanRole Role { get; set; } - public virtual bool Equals(ClanMember other) => other is not null && other.ClanId == ClanId && other.PlayerId == PlayerId; + public bool Equals(ClanMember? other) => other is not null && other.ClanId == ClanId && other.PlayerId == PlayerId; - public int CompareTo(ClanMember other) => CompareTo(other?.Role ?? ClanRole.Unknown); + public int CompareTo(ClanMember? other) => CompareTo(other?.Role ?? ClanRole.Unknown); // Alex thinks it's terrible. public int CompareTo(ClanRole other) => Role == other diff --git a/WowsKarma.Api/Data/Models/PlatformBan.cs b/WowsKarma.Api/Data/Models/PlatformBan.cs index e772584e..8c042ae9 100644 --- a/WowsKarma.Api/Data/Models/PlatformBan.cs +++ b/WowsKarma.Api/Data/Models/PlatformBan.cs @@ -1,10 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; - namespace WowsKarma.Api.Data.Models; - public sealed record PlatformBan : ITimestamped { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -12,14 +10,14 @@ public sealed record PlatformBan : ITimestamped [Required] public uint UserId { get; init; } - public Player User { get; init; } + public Player User { get; init; } = null!; [Required] public uint ModId { get; init; } - public Player Mod { get; init; } + public Player Mod { get; init; } = null!; [Required] - public string Reason { get; set; } + public string Reason { get; set; } = ""; public DateTimeOffset? BannedUntil { get; set; } diff --git a/WowsKarma.Api/Data/Models/Player.cs b/WowsKarma.Api/Data/Models/Player.cs index 6800abc3..1d3d8d53 100644 --- a/WowsKarma.Api/Data/Models/Player.cs +++ b/WowsKarma.Api/Data/Models/Player.cs @@ -1,7 +1,5 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using WowsKarma.Common; - namespace WowsKarma.Api.Data.Models; @@ -13,12 +11,12 @@ public sealed record Player : ITimestamped [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] public uint Id { get; init; } - public string Username { get; set; } + public string Username { get; set; } = ""; public DateTimeOffset CreatedAt { get; init; } public DateTimeOffset UpdatedAt { get; set; } - public ClanMember ClanMember { get; set; } + public ClanMember? ClanMember { get; set; } public bool WgHidden { get; set; } @@ -40,7 +38,7 @@ public sealed record Player : ITimestamped public bool NegativeKarmaAble => (SiteKarma + GameKarma) > NegativeKarmaAbilityThreshold; public bool PostsBanned { get; set; } public bool OptedOut { get; set; } - public DateTimeOffset OptOutChanged { get; set; } + public DateTimeOffset? OptOutChanged { get; set; } public bool IsBanned() @@ -57,16 +55,16 @@ public bool IsBanned() { Id = value.Id, Username = value.Username, - WgAccountCreatedAt = value.WgAccountCreatedAt.UtcDateTime, + WgAccountCreatedAt = value.WgAccountCreatedAt, WgHidden = value.WgHidden, OptedOut = value.OptedOut, - OptOutChanged = value.OptOutChanged.UtcDateTime, + OptOutChanged = value.OptOutChanged, GameKarma = value.GameKarma, SiteKarma = value.SiteKarma, RatingPerformance = value.PerformanceRating, RatingTeamplay = value.TeamplayRating, RatingCourtesy = value.CourtesyRating, - LastBattleTime = value.LastBattleTime.UtcDateTime + LastBattleTime = value.LastBattleTime }; public static Player MapFromApi(Player source, Player mod) diff --git a/WowsKarma.Api/Data/Models/Post.cs b/WowsKarma.Api/Data/Models/Post.cs index 4694276c..6b309f27 100644 --- a/WowsKarma.Api/Data/Models/Post.cs +++ b/WowsKarma.Api/Data/Models/Post.cs @@ -12,22 +12,22 @@ public sealed record Post : ITimestamped [Required] public uint PlayerId { get; init; } [Required] - public Player Player { get; init; } + public Player Player { get; init; } = null!; [Required] public uint AuthorId { get; init; } [Required] - public Player Author { get; init; } + public Player Author { get; init; } = null!; public PostFlairs Flairs { get; set; } - public PostFlairsParsed ParsedFlairs => Flairs.ParseFlairsEnum(); + public PostFlairsParsed? ParsedFlairs => Flairs.ParseFlairsEnum(); [Required] - public string Title { get; set; } + public string Title { get; set; } = ""; [Required] - public string Content { get; set; } + public string Content { get; set; } = ""; public Guid? ReplayId { get; set; } - public Replay Replay { get; set; } + public Replay? Replay { get; set; } // Computed by DB Engine (hopefully) public DateTimeOffset CreatedAt { get; init; } diff --git a/WowsKarma.Api/Data/Models/PostModAction.cs b/WowsKarma.Api/Data/Models/PostModAction.cs index e0a5aa6d..50a3cbe7 100644 --- a/WowsKarma.Api/Data/Models/PostModAction.cs +++ b/WowsKarma.Api/Data/Models/PostModAction.cs @@ -1,25 +1,23 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; - namespace WowsKarma.Api.Data.Models; /** * Conversion Mapping done in . **/ - -public record PostModAction +public sealed record PostModAction { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; init; } - public Post Post { get; init; } + public Post Post { get; init; } = null!; public Guid PostId { get; init; } public ModActionType ActionType { get; init; } - public Player Mod { get; init; } + public Player Mod { get; init; } = null!; public uint ModId { get; init; } - public string Reason { get; set; } + public string Reason { get; set; } = ""; } diff --git a/WowsKarma.Api/Data/Models/Replays/Replay.cs b/WowsKarma.Api/Data/Models/Replays/Replay.cs index 7ca7cbde..849f4b5b 100644 --- a/WowsKarma.Api/Data/Models/Replays/Replay.cs +++ b/WowsKarma.Api/Data/Models/Replays/Replay.cs @@ -2,20 +2,19 @@ using System.ComponentModel.DataAnnotations.Schema; using Nodsoft.WowsReplaysUnpack.Core.Models; - namespace WowsKarma.Api.Data.Models.Replays; -public record Replay +public sealed record Replay { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; init; } [Required] public Guid PostId { get; init; } - public virtual Post Post { get; init; } + public Post Post { get; init; } = null!; - public string BlobName { get; set; } + public string BlobName { get; set; } = ""; public bool MinimapRendered { get; set; } @@ -25,11 +24,11 @@ public record Replay */ [Column(TypeName = "jsonb")] - public virtual ArenaInfo ArenaInfo { get; set; } + public ArenaInfo ArenaInfo { get; set; } = null!; [Column(TypeName = "jsonb")] - public virtual IEnumerable Players { get; set; } + public IEnumerable Players { get; set; } = []; [Column(TypeName = "jsonb")] - public virtual IEnumerable ChatMessages { get; set; } + public IEnumerable ChatMessages { get; set; } = []; } diff --git a/WowsKarma.Api/Data/Models/Replays/ReplayArenaInfo.cs b/WowsKarma.Api/Data/Models/Replays/ReplayArenaInfo.cs index cb28c456..9c83bf42 100644 --- a/WowsKarma.Api/Data/Models/Replays/ReplayArenaInfo.cs +++ b/WowsKarma.Api/Data/Models/Replays/ReplayArenaInfo.cs @@ -7,16 +7,16 @@ namespace WowsKarma.Api.Data.Models.Replays; * https://dev.azure.com/wows-monitor/_git/api?path=/wows-monitor.core/appmodels/arenainfo/Arenainfo.cs */ -public record ReplayArenaInfo +public sealed record ReplayArenaInfo { public short MapId { get; set; } public int PlayerId { get; set; } - public object MatchGroup { get; set; } + public object? MatchGroup { get; set; } - public List Vehicles { get; set; } - public object DateTime { get; set; } - public string Token { get; set; } + public List Vehicles { get; set; } = []; + public object? DateTime { get; set; } + public string? Token { get; set; } public Region Region { get; set; } @@ -37,11 +37,10 @@ public record ReplayArenaInfo //public string Logic { get; set; } //public string PlayerVehicle { get; set; } - [JsonExtensionData] - public Dictionary ExtendedData { get; set; } + [JsonExtensionData] public Dictionary ExtendedData { get; set; } = []; } -public record Ship : IHasRelation +public sealed record Ship : IHasRelation { public int Id { get; set; } @@ -49,6 +48,6 @@ public record Ship : IHasRelation public Relation Relation { get; set; } - public string Name { get; set; } + public string Name { get; set; } = ""; } diff --git a/WowsKarma.Api/Data/Models/Replays/ReplayPlayer.cs b/WowsKarma.Api/Data/Models/Replays/ReplayPlayer.cs index 2dc23f9e..e1f47861 100644 --- a/WowsKarma.Api/Data/Models/Replays/ReplayPlayer.cs +++ b/WowsKarma.Api/Data/Models/Replays/ReplayPlayer.cs @@ -8,8 +8,8 @@ public readonly struct ReplayPlayer public uint AccountId { get; init; } public string Name { get; init; } - public uint ClanId { get; init; } - public string ClanTag { get; init; } + public uint? ClanId { get; init; } + public string? ClanTag { get; init; } public uint TeamId { get; init; } diff --git a/WowsKarma.Api/Hubs/AuthHub.cs b/WowsKarma.Api/Hubs/AuthHub.cs index c1656749..9a46ad0b 100644 --- a/WowsKarma.Api/Hubs/AuthHub.cs +++ b/WowsKarma.Api/Hubs/AuthHub.cs @@ -3,7 +3,7 @@ namespace WowsKarma.Api.Hubs; -public class AuthHub : Hub, IAuthHubInvoke +public sealed class AuthHub : Hub, IAuthHubInvoke { } diff --git a/WowsKarma.Api/Hubs/NotificationsHub.cs b/WowsKarma.Api/Hubs/NotificationsHub.cs index 086ac3df..71b584ed 100644 --- a/WowsKarma.Api/Hubs/NotificationsHub.cs +++ b/WowsKarma.Api/Hubs/NotificationsHub.cs @@ -2,18 +2,14 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using System.Runtime.CompilerServices; -using System.Threading; using WowsKarma.Api.Data.Models.Notifications; using WowsKarma.Api.Services; using WowsKarma.Common.Hubs; - - namespace WowsKarma.Api.Hubs; - [Authorize] -public class NotificationsHub : Hub, INotificationsHubInvoke +public sealed class NotificationsHub : Hub, INotificationsHubInvoke { private readonly NotificationService _service; @@ -24,7 +20,10 @@ public NotificationsHub(NotificationService service) public Task AcknowledgeNotifications(Guid[] notificationIds) { - IQueryable notifications = _service.GetNotifications(notificationIds).Where(n => n.AccountId == uint.Parse(Context.UserIdentifier)); + uint userId = uint.Parse(Context.UserIdentifier ?? throw new InvalidOperationException("No context user identifier. Is the user logged in on the hub?")); + + IQueryable notifications = _service.GetNotifications(notificationIds).Where(n => n.AccountId == userId); + _service.AcknowledgeNotifications(notifications); return Task.CompletedTask; } @@ -43,7 +42,7 @@ public Task AcknowledgeNotifications(Guid[] notificationIds) ct.ThrowIfCancellationRequested(); object notificationDto = item.ToDTO(); - yield return (notificationDto.GetType().FullName, notificationDto); + yield return (notificationDto.GetType().FullName!, notificationDto); } } } diff --git a/WowsKarma.Api/Hubs/PostHub.cs b/WowsKarma.Api/Hubs/PostHub.cs index 4fc3d520..522c53de 100644 --- a/WowsKarma.Api/Hubs/PostHub.cs +++ b/WowsKarma.Api/Hubs/PostHub.cs @@ -3,7 +3,7 @@ namespace WowsKarma.Api.Hubs; -public class PostHub : Hub, IPostHubInvoke +public sealed class PostHub : Hub, IPostHubInvoke { } \ No newline at end of file diff --git a/WowsKarma.Api/Infrastructure/Attributes/ETagAttribute.cs b/WowsKarma.Api/Infrastructure/Attributes/ETagAttribute.cs index f1ff2206..0f67cd71 100644 --- a/WowsKarma.Api/Infrastructure/Attributes/ETagAttribute.cs +++ b/WowsKarma.Api/Infrastructure/Attributes/ETagAttribute.cs @@ -4,7 +4,7 @@ /// Attribute for controlling ETag generation for a given endpoint. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] -public class ETagAttribute : Attribute +public sealed class ETagAttribute : Attribute { /// /// Initializes a new instance of the class. diff --git a/WowsKarma.Api/Infrastructure/Authorization/HangfireDashboardAuthorizationFilter.cs b/WowsKarma.Api/Infrastructure/Authorization/HangfireDashboardAuthorizationFilter.cs index f5d9a8fe..2816bea8 100644 --- a/WowsKarma.Api/Infrastructure/Authorization/HangfireDashboardAuthorizationFilter.cs +++ b/WowsKarma.Api/Infrastructure/Authorization/HangfireDashboardAuthorizationFilter.cs @@ -1,6 +1,5 @@ using System.Security.Claims; using Hangfire.Dashboard; -using Microsoft.AspNetCore.Http; using WowsKarma.Common; namespace WowsKarma.Api.Infrastructure.Authorization; @@ -9,7 +8,7 @@ namespace WowsKarma.Api.Infrastructure.Authorization; /// Simple RBAC auth filter to check if the user has the Admin role, and grant access to the Hangfire dashboard if so. /// Also grants readonly access to the Hangfire dashboard if the user has the CM role. /// -public class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter +public sealed class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter { public static readonly HangfireDashboardAuthorizationFilter Instance = new(); diff --git a/WowsKarma.Api/Infrastructure/Authorization/PlatformBanAuthorizationHandler.cs b/WowsKarma.Api/Infrastructure/Authorization/PlatformBanAuthorizationHandler.cs index d198d1dd..ba7ec615 100644 --- a/WowsKarma.Api/Infrastructure/Authorization/PlatformBanAuthorizationHandler.cs +++ b/WowsKarma.Api/Infrastructure/Authorization/PlatformBanAuthorizationHandler.cs @@ -1,14 +1,11 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using WowsKarma.Api.Data; namespace WowsKarma.Api.Infrastructure.Authorization; -#nullable enable -public class PlatformBanAuthorizationHandler : AuthorizationHandler +public sealed class PlatformBanAuthorizationHandler : AuthorizationHandler { private readonly ILogger _logger; private readonly ApiDbContext _dbContext; diff --git a/WowsKarma.Api/Infrastructure/Authorization/PlatformBanRequirement.cs b/WowsKarma.Api/Infrastructure/Authorization/PlatformBanRequirement.cs index 4fc36f44..2783e78b 100644 --- a/WowsKarma.Api/Infrastructure/Authorization/PlatformBanRequirement.cs +++ b/WowsKarma.Api/Infrastructure/Authorization/PlatformBanRequirement.cs @@ -5,7 +5,7 @@ namespace WowsKarma.Api.Infrastructure.Authorization; /// /// Provides an authorization requirement to evaluate a user's platform bans. /// -public class PlatformBanRequirement : IAuthorizationRequirement +public sealed class PlatformBanRequirement : IAuthorizationRequirement { /// /// Whether the user should be banned from the current platform. diff --git a/WowsKarma.Api/Infrastructure/Data/Page.cs b/WowsKarma.Api/Infrastructure/Data/Page.cs index a0883e60..7bd99ff0 100644 --- a/WowsKarma.Api/Infrastructure/Data/Page.cs +++ b/WowsKarma.Api/Infrastructure/Data/Page.cs @@ -1,7 +1,5 @@ namespace WowsKarma.Api.Infrastructure.Data; -#nullable enable - /// /// Represents a paged list of items. /// diff --git a/WowsKarma.Api/Infrastructure/Telemetry/HubTelemetryFilter.cs b/WowsKarma.Api/Infrastructure/Telemetry/HubTelemetryFilter.cs index f93755ab..afc06e2b 100644 --- a/WowsKarma.Api/Infrastructure/Telemetry/HubTelemetryFilter.cs +++ b/WowsKarma.Api/Infrastructure/Telemetry/HubTelemetryFilter.cs @@ -4,7 +4,7 @@ namespace WowsKarma.Api.Infrastructure.Telemetry; -public class HubTelemetryFilter : ITelemetryProcessor +public sealed class HubTelemetryFilter : ITelemetryProcessor { private ITelemetryProcessor Next { get; set; } @@ -15,7 +15,7 @@ public HubTelemetryFilter(ITelemetryProcessor next) public void Process(ITelemetry item) { - if (item is RequestTelemetry request and { Name: not null } && request.Name.Contains("hub")) + if (item is RequestTelemetry { Name: not null } request && request.Name.Contains("hub")) { return; } diff --git a/WowsKarma.Api/Infrastructure/Telemetry/TelemetryEnrichment.cs b/WowsKarma.Api/Infrastructure/Telemetry/TelemetryEnrichment.cs index 5843262f..2d40de28 100644 --- a/WowsKarma.Api/Infrastructure/Telemetry/TelemetryEnrichment.cs +++ b/WowsKarma.Api/Infrastructure/Telemetry/TelemetryEnrichment.cs @@ -1,7 +1,6 @@ using Microsoft.ApplicationInsights.AspNetCore.TelemetryInitializers; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.AspNetCore.Http; using WowsKarma.Common; namespace WowsKarma.Api.Infrastructure.Telemetry; @@ -17,14 +16,14 @@ protected override void OnInitializeTelemetry(HttpContext platformContext, Reque telemetry.Context.GlobalProperties["api-region"] = Startup.ApiRegion.ToRegionString(); // User ID - AccountListingDTO userAccount = platformContext.User?.ToAccountListing(); + AccountListingDTO? userAccount = platformContext.User.ToAccountListing(); telemetry.Context.User.AuthenticatedUserId = userAccount?.Id.ToString() ?? string.Empty; // IP Address if (telemetry is ISupportProperties propTelemetry && !propTelemetry.Properties.ContainsKey("client-ip")) { - string clientIPValue = telemetry.Context.Location.Ip; - propTelemetry.Properties.Add("client-ip", clientIPValue); + string clientIpValue = telemetry.Context.Location.Ip; + propTelemetry.Properties.Add("client-ip", clientIpValue); } } } diff --git a/WowsKarma.Api/Middlewares/ETagMiddleware.cs b/WowsKarma.Api/Middlewares/ETagMiddleware.cs index 14d0ca6c..be223d0c 100644 --- a/WowsKarma.Api/Middlewares/ETagMiddleware.cs +++ b/WowsKarma.Api/Middlewares/ETagMiddleware.cs @@ -1,8 +1,5 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.WebUtilities; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; -using System.IO; using System.Security.Cryptography; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; @@ -13,7 +10,7 @@ namespace WowsKarma.Api.Middlewares; /// /// Provides an ETag generation middleware. /// -public class ETagMiddleware +public sealed class ETagMiddleware { private readonly RequestDelegate _next; diff --git a/WowsKarma.Api/Middlewares/RequestLoggingMiddleware.cs b/WowsKarma.Api/Middlewares/RequestLoggingMiddleware.cs index 1a777e44..eefe1ef6 100644 --- a/WowsKarma.Api/Middlewares/RequestLoggingMiddleware.cs +++ b/WowsKarma.Api/Middlewares/RequestLoggingMiddleware.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features; using Serilog; using Serilog.Events; using System.Diagnostics; @@ -8,18 +7,18 @@ namespace WowsKarma.Api.Middlewares; -public class RequestLoggingMiddleware +public sealed class RequestLoggingMiddleware { private const string MessageTemplate = "{Protocol} {RequestMethod} {RequestPath} by {RemoteUser}, responded {StatusCode} in {Elapsed:0.00} ms"; - private static readonly ILogger logger = Log.ForContext(); - private static readonly HashSet HeaderWhitelist = new() { "Content-Type", "Content-Length", "User-Agent" }; + private static readonly ILogger _logger = Log.ForContext(); + private static readonly HashSet _headerWhitelist = new() { "Content-Type", "Content-Length", "User-Agent" }; - private readonly RequestDelegate next; + private readonly RequestDelegate _next; public RequestLoggingMiddleware(RequestDelegate next) { - this.next = next ?? throw new ArgumentNullException(nameof(next)); + _next = next ?? throw new ArgumentNullException(nameof(next)); } @@ -31,15 +30,15 @@ public async Task Invoke(HttpContext context) try { - await next(context); + await _next(context); double elapsedMs = GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()); - int? statusCode = context.Response?.StatusCode; + int? statusCode = context.Response.StatusCode; LogEventLevel level = statusCode > 499 ? LogEventLevel.Error : LogEventLevel.Information; ILogger log = level is LogEventLevel.Error ? LogForErrorContext(context) - : logger.ForContext("RequestUser", GetRemoteUser(context)); + : _logger.ForContext("RequestUser", GetRemoteUser(context)); log.Write(level, MessageTemplate, context.Request.Protocol, context.Request.Method, GetPath(context), GetRemoteUser(context), statusCode, elapsedMs); } @@ -60,10 +59,10 @@ private static ILogger LogForErrorContext(HttpContext context) HttpRequest request = context.Request; Dictionary loggedHeaders = request.Headers - .Where(h => HeaderWhitelist.Contains(h.Key)) + .Where(h => _headerWhitelist.Contains(h.Key)) .ToDictionary(h => h.Key, h => h.Value.ToString()); - return logger + return _logger .ForContext("RequestHeaders", loggedHeaders, destructureObjects: true) .ForContext("RequestHost", request.Host) .ForContext("RequestProtocol", request.Protocol); @@ -73,5 +72,5 @@ private static ILogger LogForErrorContext(HttpContext context) private static string GetPath(HttpContext context) => context.Features.Get()?.RawTarget ?? context.Request.Path.ToString(); - private static string GetRemoteUser(HttpContext context) => context.User?.FindFirstValue(ClaimTypes.Name) ?? context.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; + private static string GetRemoteUser(HttpContext context) => context.User.FindFirstValue(ClaimTypes.Name) ?? context.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; } \ No newline at end of file diff --git a/WowsKarma.Api/Migrations/ApiDb/20220311183423_AddClans.cs b/WowsKarma.Api/Migrations/ApiDb/20220311183423_AddClans.cs index 7218cc2a..540309dd 100644 --- a/WowsKarma.Api/Migrations/ApiDb/20220311183423_AddClans.cs +++ b/WowsKarma.Api/Migrations/ApiDb/20220311183423_AddClans.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows; #nullable disable diff --git a/WowsKarma.Api/Migrations/ApiDb/20220312144159_ReplaceClanMemberNavigation.cs b/WowsKarma.Api/Migrations/ApiDb/20220312144159_ReplaceClanMemberNavigation.cs index 0ba4c877..9ba99ba4 100644 --- a/WowsKarma.Api/Migrations/ApiDb/20220312144159_ReplaceClanMemberNavigation.cs +++ b/WowsKarma.Api/Migrations/ApiDb/20220312144159_ReplaceClanMemberNavigation.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/WowsKarma.Api/Migrations/ApiDb/20220730123556_AddModEditedNotification.cs b/WowsKarma.Api/Migrations/ApiDb/20220730123556_AddModEditedNotification.cs index 401d153e..c88e5a35 100644 --- a/WowsKarma.Api/Migrations/ApiDb/20220730123556_AddModEditedNotification.cs +++ b/WowsKarma.Api/Migrations/ApiDb/20220730123556_AddModEditedNotification.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/WowsKarma.Api/Migrations/AuthDb/20220309215406_UpdateHeuristicsFormat.cs b/WowsKarma.Api/Migrations/AuthDb/20220309215406_UpdateHeuristicsFormat.cs index f42c3dac..0724d2c1 100644 --- a/WowsKarma.Api/Migrations/AuthDb/20220309215406_UpdateHeuristicsFormat.cs +++ b/WowsKarma.Api/Migrations/AuthDb/20220309215406_UpdateHeuristicsFormat.cs @@ -1,6 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/WowsKarma.Api/Program.cs b/WowsKarma.Api/Program.cs index 8cc7e212..72089676 100644 --- a/WowsKarma.Api/Program.cs +++ b/WowsKarma.Api/Program.cs @@ -1,10 +1,5 @@ -using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Serilog; -using System.IO; -using Serilog.Events; using WowsKarma.Api.Data; using WowsKarma.Api.Utilities; using WowsKarma.Common; @@ -20,7 +15,7 @@ public static async Task Main(string[] args) using IHost host = CreateHostBuilder(args).Build(); using IServiceScope scope = host.Services.CreateScope(); - Log.Information("Region selected : {Region}", Startup.ApiRegion); + Log.Information("Region selected : {region}", Startup.ApiRegion); await using (ApiDbContext db = scope.ServiceProvider.GetRequiredService()) { await db.Database.MigrateAsync(); diff --git a/WowsKarma.Api/Services/Authentication/Cookie/ForwardCookieAuthenticationHandler.cs b/WowsKarma.Api/Services/Authentication/Cookie/ForwardCookieAuthenticationHandler.cs index 7cc6b553..d660660c 100644 --- a/WowsKarma.Api/Services/Authentication/Cookie/ForwardCookieAuthenticationHandler.cs +++ b/WowsKarma.Api/Services/Authentication/Cookie/ForwardCookieAuthenticationHandler.cs @@ -1,12 +1,7 @@ -using System.Net.Http.Headers; -using System.Text.Encodings.Web; +using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using WowsKarma.Api.Services.Authentication.Jwt; using WowsKarma.Common; @@ -16,19 +11,20 @@ namespace WowsKarma.Api.Services.Authentication.Cookie; /// Provides a cookie-based authentication implementation, /// forwarding any authentication cookie to the Bearer token system. /// -public class ForwardCookieAuthenticationHandler : JwtAuthenticationHandler +public sealed class ForwardCookieAuthenticationHandler : JwtAuthenticationHandler { private readonly string _cookieName; public ForwardCookieAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, UserService userService, IConfiguration configuration) : base(options, logger, encoder, clock, userService) { - _cookieName = configuration[$"API:{Startup.ApiRegion.ToRegionString()}:CookieName"]; + _cookieName = configuration[$"API:{Startup.ApiRegion.ToRegionString()}:CookieName"] + ?? throw new($"Missing configuration value for API:{Startup.ApiRegion.ToRegionString()}:CookieName"); } protected override Task HandleAuthenticateAsync() { - if (Request.Cookies.TryGetValue(_cookieName, out string cookie)) + if (Request.Cookies.TryGetValue(_cookieName, out string? cookie)) { Request.Headers.Authorization = new($"Bearer {cookie}"); } diff --git a/WowsKarma.Api/Services/Authentication/Jwt/ApiRole.cs b/WowsKarma.Api/Services/Authentication/Jwt/ApiRole.cs index 777698f1..514c8b69 100644 --- a/WowsKarma.Api/Services/Authentication/Jwt/ApiRole.cs +++ b/WowsKarma.Api/Services/Authentication/Jwt/ApiRole.cs @@ -1,8 +1,10 @@ -using Microsoft.AspNetCore.Identity; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Identity; namespace WowsKarma.Api.Services.Authentication.Jwt; -public class ApiRole : IdentityRole +[UsedImplicitly] +public sealed class ApiRole : IdentityRole { public ApiRole() : base() { } public ApiRole(string name) : base(name) diff --git a/WowsKarma.Api/Services/Authentication/Jwt/ApplicationUser.cs b/WowsKarma.Api/Services/Authentication/Jwt/ApplicationUser.cs index ae3147f5..56510e32 100644 --- a/WowsKarma.Api/Services/Authentication/Jwt/ApplicationUser.cs +++ b/WowsKarma.Api/Services/Authentication/Jwt/ApplicationUser.cs @@ -1,7 +1,9 @@ -using Microsoft.AspNetCore.Identity; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Identity; namespace WowsKarma.Api.Services.Authentication.Jwt; +[UsedImplicitly] public class ApplicationUser : IdentityUser { public ApplicationUser() : base() { } diff --git a/WowsKarma.Api/Services/Authentication/Jwt/JwtAuthenticationHandler.cs b/WowsKarma.Api/Services/Authentication/Jwt/JwtAuthenticationHandler.cs index c994c7da..51fc1a12 100644 --- a/WowsKarma.Api/Services/Authentication/Jwt/JwtAuthenticationHandler.cs +++ b/WowsKarma.Api/Services/Authentication/Jwt/JwtAuthenticationHandler.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Security.Claims; using System.Text.Encodings.Web; @@ -31,8 +30,9 @@ protected override async Task HandleAuthenticateAsync() try { - if (new Guid(baseResult.Principal.FindFirstValue("seed")) is var seed - && await userService.ValidateUserSeedTokenAsync(uint.Parse(baseResult.Principal.FindFirstValue(ClaimTypes.NameIdentifier)), seed)) + if (baseResult.Principal.FindFirstValue("seed") is { } seed + && uint.TryParse(baseResult.Principal.FindFirstValue(ClaimTypes.NameIdentifier), out uint id) + && await userService.ValidateUserSeedTokenAsync(id, new(seed))) { isValid = true; } diff --git a/WowsKarma.Api/Services/Authentication/Jwt/JwtService.cs b/WowsKarma.Api/Services/Authentication/Jwt/JwtService.cs index 51b3c7a5..8fc454e0 100644 --- a/WowsKarma.Api/Services/Authentication/Jwt/JwtService.cs +++ b/WowsKarma.Api/Services/Authentication/Jwt/JwtService.cs @@ -3,39 +3,37 @@ using System.Security.Claims; using System.Text; - - namespace WowsKarma.Api.Services.Authentication.Jwt; -public class JwtService +public sealed class JwtService { internal JwtSecurityTokenHandler TokenHandler { get; private init; } - private static IConfiguration configuration; - private static SymmetricSecurityKey authSigningKey; + private static IConfiguration _configuration = null!; + private static SymmetricSecurityKey _authSigningKey = null!; public JwtService(IConfiguration configuration, JwtSecurityTokenHandler handler) { - JwtService.configuration ??= configuration; + _configuration = configuration; TokenHandler = handler; - authSigningKey = new(Encoding.UTF8.GetBytes(configuration["JWT:Secret"])); + _authSigningKey = new(Encoding.UTF8.GetBytes(configuration["JWT:Secret"] ?? throw new("Missing JWT:Secret configuration value"))); } public static JwtSecurityToken GenerateToken(IEnumerable authClaims) => new( - issuer: configuration["JWT:ValidIssuer"], - audience: configuration["JWT:ValidAudience"], + issuer: _configuration["JWT:ValidIssuer"], + audience: _configuration["JWT:ValidAudience"], expires: DateTime.UtcNow.AddDays(8), claims: authClaims, - signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)); + signingCredentials: new(_authSigningKey, SecurityAlgorithms.HmacSha256)); - public ClaimsPrincipal ValidateToken(string token) + public ClaimsPrincipal? ValidateToken(string token) { TokenValidationParameters validationParameters = new() { - IssuerSigningKey = authSigningKey, - ValidAudience = configuration["JWT:ValidAudience"], - ValidIssuer = configuration["JWT:ValidIssuer"], + IssuerSigningKey = _authSigningKey, + ValidAudience = _configuration["JWT:ValidAudience"], + ValidIssuer = _configuration["JWT:ValidIssuer"], ValidateLifetime = true, ValidateAudience = true, ValidateIssuer = true, diff --git a/WowsKarma.Api/Services/Authentication/UserService.cs b/WowsKarma.Api/Services/Authentication/UserService.cs index 54b70477..a9037984 100644 --- a/WowsKarma.Api/Services/Authentication/UserService.cs +++ b/WowsKarma.Api/Services/Authentication/UserService.cs @@ -7,32 +7,44 @@ using WowsKarma.Api.Hubs; using WowsKarma.Api.Services.Authentication.Jwt; using WowsKarma.Api.Services.Authentication.Wargaming; -using WowsKarma.Common; using WowsKarma.Common.Hubs; namespace WowsKarma.Api.Services.Authentication; -public class UserService +/// +/// Provides a service to fetch and update API users. +/// +public sealed class UserService { private const string SeedTokenClaimType = "seed"; - private readonly AuthDbContext context; + private readonly AuthDbContext _context; private readonly IHubContext _hubContext; public UserService(AuthDbContext context, IHubContext hubContext) { - this.context = context; + _context = context; _hubContext = hubContext; } - public Task GetUserAsync(uint id) => context.Users.Include(u => u.Roles).FirstOrDefaultAsync(u => u.Id == id); - - public async Task> GetUserClaimsAsync(uint id) => await GetUserAsync(id) is { } user - ? from role in user.Roles select new Claim(ClaimTypes.Role, role.InternalName) - : Enumerable.Empty(); + /// + /// Gets a user by their ID. + /// + /// The user's ID. + /// The user, or if not found. + public Task GetUserAsync(uint id) => _context.Users.Include(u => u.Roles).FirstOrDefaultAsync(u => u.Id == id); + + /// + /// Gets a user's claims + /// + /// + /// + public async Task> GetUserClaimsAsync(uint id) => await GetUserAsync(id) is { Roles: [..] roles } + ? from role in roles select new Claim(ClaimTypes.Role, role.InternalName) + : []; public async Task GetUserSeedTokenAsync(uint id) { - if (await context.Users.FindAsync(id) is not { } user) + if (await _context.Users.FindAsync(id) is not { } user) { user = new() { @@ -40,22 +52,22 @@ public async Task GetUserSeedTokenAsync(uint id) SeedToken = Guid.NewGuid() }; - await context.Users.AddAsync(user); + await _context.Users.AddAsync(user); } user.LastTokenRequested = DateTimeOffset.UtcNow; - await context.SaveChangesAsync(); + await _context.SaveChangesAsync(); return user.SeedToken; } - public async Task ValidateUserSeedTokenAsync(uint id, Guid seedToken) => await context.Users.FindAsync(id) is { } user && user.SeedToken == seedToken; + public async Task ValidateUserSeedTokenAsync(uint id, Guid seedToken) => await _context.Users.FindAsync(id) is { SeedToken: var st } && st == seedToken; public async Task RenewSeedTokenAsync(uint id) { if (await GetUserAsync(id) is { } user) { user.SeedToken = Guid.NewGuid(); - await context.SaveChangesAsync(); + await _context.SaveChangesAsync(); await _hubContext.Clients.All.SeedTokenInvalidated(user.Id); } @@ -65,7 +77,7 @@ public async Task CreateTokenAsync(WargamingIdentity identity) { (uint id, _) = identity.GetAccountListing(); - if (identity.Claims.Where(c => c.Type is ClaimTypes.Role).ToArray() is { Length: > 0 } claims) + if (identity.Claims.Where(c => c.Type is ClaimTypes.Role).ToArray() is { Length: not 0 } claims) { foreach (Claim c in claims) { diff --git a/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthClientFactory.cs b/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthClientFactory.cs index 0b1d6300..307a3233 100644 --- a/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthClientFactory.cs +++ b/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthClientFactory.cs @@ -1,17 +1,16 @@ -using System.Net.Http; -using Nodsoft.Wargaming.Api.Common; +using Nodsoft.Wargaming.Api.Common; using WowsKarma.Common; namespace WowsKarma.Api.Services.Authentication.Wargaming; -public class WargamingAuthClientFactory +public sealed class WargamingAuthClientFactory { - private readonly IHttpClientFactory httpClientFactory; + private readonly IHttpClientFactory _httpClientFactory; public WargamingAuthClientFactory(IHttpClientFactory httpClientFactory) { - this.httpClientFactory = httpClientFactory; + _httpClientFactory = httpClientFactory; } - public HttpClient GetClient(Region region) => httpClientFactory.CreateClient("wargaming-auth-" + region.ToRegionString()); + public HttpClient GetClient(Region region) => _httpClientFactory.CreateClient($"wargaming-auth-{region.ToRegionString()}"); } \ No newline at end of file diff --git a/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthExtensions.cs b/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthExtensions.cs index e67eb821..bd741866 100644 --- a/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthExtensions.cs +++ b/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthExtensions.cs @@ -1,10 +1,8 @@ -using System.Net.Http; -using WowsKarma.Api.Services.Authentication.Wargaming; +using WowsKarma.Api.Services.Authentication.Wargaming; using WowsKarma.Common; using WowsKarma.Api; - - +// ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; public static class WargamingAuthExtensions @@ -16,9 +14,10 @@ public static IServiceCollection AddWargamingAuth(this IServiceCollection servic services.AddHttpClient($"wargaming-auth-{Startup.ApiRegion.ToRegionString()}", c => { - c.BaseAddress = new Uri($"https://{Startup.ApiRegion.ToWargamingSubdomain()}.wargaming.net"); + c.BaseAddress = new($"https://{Startup.ApiRegion.ToWargamingSubdomain()}.wargaming.net"); c.DefaultRequestHeaders.Add("Accept", "application/json"); - }).ConfigureHttpMessageHandlerBuilder(c => c.PrimaryHandler = new HttpClientHandler() { MaxConnectionsPerServer = 200, UseProxy = false }); + }) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { MaxConnectionsPerServer = 200, UseProxy = false }); return services; } diff --git a/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthService.cs b/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthService.cs index b40a714d..89cef0b3 100644 --- a/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthService.cs +++ b/WowsKarma.Api/Services/Authentication/Wargaming/WargamingAuthService.cs @@ -1,50 +1,41 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System.Net.Http; +using Microsoft.AspNetCore.Mvc; using WowsKarma.Common; using static WowsKarma.Common.Utilities; - - namespace WowsKarma.Api.Services.Authentication.Wargaming; -internal class WargamingAuthConfig -{ - public string VerifyIdentityUri { get; set; } -} - -public class WargamingAuthService +public sealed class WargamingAuthService { - public static Uri OpenIdDomain { get; } = new($"https://{Startup.ApiRegion.ToWargamingSubdomain()}.wargaming.net/id/openid"); + private static readonly Uri _openIdDomain = new($"https://{Startup.ApiRegion.ToWargamingSubdomain()}.wargaming.net/id/openid"); - private static string callbackUrl; + private static string? _callbackUrl; - private readonly WargamingAuthClientFactory authClientFactory; + private readonly WargamingAuthClientFactory _authClientFactory; - public WargamingAuthService(IConfiguration configuration, ILogger logger, WargamingAuthClientFactory authClientFactory) + public WargamingAuthService(IConfiguration configuration, WargamingAuthClientFactory authClientFactory) { - callbackUrl ??= configuration[$"Api:{Startup.ApiRegion.ToRegionString()}:WgAuthCallback"]; - this.authClientFactory = authClientFactory; + _callbackUrl ??= configuration[$"Api:{Startup.ApiRegion.ToRegionString()}:WgAuthCallback"]; + _authClientFactory = authClientFactory; } - public static IActionResult RedirectToLogin(IDictionary extraRedirectParams = null) => new RedirectResult(GetAuthUri(extraRedirectParams).ToString()); + public static IActionResult RedirectToLogin(IReadOnlyDictionary? extraRedirectParams = null) => new RedirectResult(GetAuthUri(extraRedirectParams).ToString()); - public static Uri GetAuthUri(IDictionary extraRedirectParams = null) + public static Uri GetAuthUri(IReadOnlyDictionary? extraRedirectParams = null) { - string verifyIdentityUri = callbackUrl; + string verifyIdentityUri = _callbackUrl!; - if (extraRedirectParams?.Any() is true) + if (extraRedirectParams is { Count: not 0 }) { - string queryString = string.Join('&', extraRedirectParams - .Where(e => !string.IsNullOrEmpty(e.Value)) - .Select(param => $"{param.Key}={param.Value}")); + string queryString = string.Join('&', + from e in extraRedirectParams + where e is { Value: not (null or "") } + select $"{e.Key}={e.Value}"); verifyIdentityUri += $"?{queryString}"; } - UriBuilder builder = new(OpenIdDomain) + UriBuilder builder = new(_openIdDomain) { Query = BuildQuery( ("openid.ns", "http://specs.openid.net/auth/2.0"), @@ -63,19 +54,19 @@ public async Task VerifyIdentity(HttpRequest context) { // https://eu.wargaming.net/id/503276471-cpt_stewie/ - Dictionary paramDict = context.Query.ToDictionary(kv => kv.Key, kv => kv.Value.FirstOrDefault()); + Dictionary paramDict = context.Query.ToDictionary(kv => kv.Key, kv => kv.Value.FirstOrDefault()); bool isValid = await IsValid(paramDict); return isValid; } - private async Task IsValid(IDictionary paramDict) + private async Task IsValid(IDictionary paramDict) { paramDict["openid.mode"] = "check_authentication"; - using HttpClient httpClient = authClientFactory.GetClient(Startup.ApiRegion); - using HttpResponseMessage response = await httpClient.PostAsync("id/openid" + paramDict.BuildQuery(), null); + using HttpClient httpClient = _authClientFactory.GetClient(Startup.ApiRegion); + using HttpResponseMessage response = await httpClient.PostAsync($"id/openid{paramDict.BuildQuery()}", null); string stringResponse = await response.Content.ReadAsStringAsync(); return stringResponse.Contains("is_valid:true"); } diff --git a/WowsKarma.Api/Services/Authentication/Wargaming/WargamingIdentity.cs b/WowsKarma.Api/Services/Authentication/Wargaming/WargamingIdentity.cs index 29a55654..f8ced58e 100644 --- a/WowsKarma.Api/Services/Authentication/Wargaming/WargamingIdentity.cs +++ b/WowsKarma.Api/Services/Authentication/Wargaming/WargamingIdentity.cs @@ -4,7 +4,7 @@ namespace WowsKarma.Api.Services.Authentication.Wargaming; -public class WargamingIdentity : ClaimsIdentity +public sealed class WargamingIdentity : ClaimsIdentity { public new const string AuthenticationType = "Wargaming"; @@ -18,22 +18,23 @@ public static WargamingIdentity FromUri(Uri identityUri) string accountId = segment[..index]; string nickname = segment[(index + 1)..^1].Replace("/", string.Empty); - List claims = new() - { + List claims = + [ new(ClaimTypes.NameIdentifier, accountId), new(ClaimTypes.Name, nickname), new(WargamingClaimTypes.Region, ((int)region).ToString()), new(WargamingClaimTypes.RegionName, region.ToString()) - }; + ]; return new(claims); } - public AccountListingDTO GetAccountListing() + public AccountListingDTO? GetAccountListing() { - if (uint.TryParse(Claims.FirstOrDefault(c => c.Type is ClaimTypes.NameIdentifier)?.Value, out uint id)) + if (uint.TryParse(Claims.FirstOrDefault(c => c.Type is ClaimTypes.NameIdentifier)?.Value, out uint id) + && Claims.FirstOrDefault(c => c.Type is ClaimTypes.Name) is { Value: { Length: not 0 } name }) { - return new(id, Claims.FirstOrDefault(c => c.Type is ClaimTypes.Name)?.Value); + return new(id, name); } return null; diff --git a/WowsKarma.Api/Services/ClanService.cs b/WowsKarma.Api/Services/ClanService.cs index 6be57dfa..eda2b7ce 100644 --- a/WowsKarma.Api/Services/ClanService.cs +++ b/WowsKarma.Api/Services/ClanService.cs @@ -1,19 +1,17 @@ using System.ComponentModel.DataAnnotations; using System.Drawing; -using System.Threading; using Mapster; using Microsoft.EntityFrameworkCore; using Nodsoft.Wargaming.Api.Client.Clients.Wows; using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows.Clans; using WowsKarma.Api.Data; using WowsKarma.Api.Utilities; -using WowsKarma.Common; using ApiClanMember = Nodsoft.Wargaming.Api.Common.Data.Responses.Wows.Clans.ClanMember; using ClanMember = WowsKarma.Api.Data.Models.ClanMember; namespace WowsKarma.Api.Services; -public class ClanService +public sealed class ClanService { public static TimeSpan ClanInfoUpdateSpan { get; } = TimeSpan.FromHours(4); public static TimeSpan ClanMemberUpdateSpan { get; } = TimeSpan.FromHours(4); @@ -51,11 +49,11 @@ public async Task> SearchClansAsync([MinLength(2)] s Name = x.Name, Tag = x.Tag, LeagueColor = (uint)ColorTranslator.FromHtml(x.HexColor).ToArgb() - }); + }) ?? []; - public async Task GetClanAsync(uint clanId, bool includeMembers = false, CancellationToken ct = default) + public async Task GetClanAsync(uint clanId, bool includeMembers = false, CancellationToken ct = default) { - Clan clan = await GetDbClans(includeMembers).AsNoTracking().FirstOrDefaultAsync(c => c.Id == clanId, ct); + Clan? clan = await GetDbClans(includeMembers).AsNoTracking().FirstOrDefaultAsync(c => c.Id == clanId, ct); bool updateInfo = clan is null || ClanInfoUpdateNeeded(clan); bool updateMembers = includeMembers && (clan is null || ClanMembersUpdateNeeded(clan)); @@ -64,7 +62,7 @@ public async Task GetClanAsync(uint clanId, bool includeMembers = false, C clan = await UpdateClanInfoAsync(_context, clanId, clan, ct); } - if (updateMembers) + if (updateMembers && clan is not null) { clan = await UpdateClanMembersAsync(_context, clan, ct); } @@ -72,15 +70,15 @@ public async Task GetClanAsync(uint clanId, bool includeMembers = false, C return clan; } - internal async Task UpdateClanInfoAsync(ApiDbContext context, uint clanId, Clan clan, CancellationToken ct) + internal async Task UpdateClanInfoAsync(ApiDbContext context, uint clanId, Clan? clan, CancellationToken ct) { - ClanInfo apiClan = (await _clansApi.FetchClanViewAsync(clanId, ct))?.Clan; + ClanInfo? apiClan = (await _clansApi.FetchClanViewAsync(clanId, ct))?.Clan; if (clan is null) { clan = apiClan?.Adapt(); } - else + else if (apiClan is not null) { clan = clan with { @@ -91,8 +89,11 @@ internal async Task UpdateClanInfoAsync(ApiDbContext context, uint clanId, }; } - clan!.UpdatedAt = DateTimeOffset.UtcNow; - await context.Clans.Upsert(clan).On(c => c.Id).RunAsync(ct); + if (clan is not null) + { + clan.UpdatedAt = DateTimeOffset.UtcNow; + await context.Clans.Upsert(clan).On(c => c.Id).RunAsync(ct); + } return clan; } @@ -110,7 +111,7 @@ internal async Task UpdateClanMembersAsync(ApiDbContext context, Clan clan foreach (uint id in missing) { - Player dbPlayer = (await _vortex.FetchAccountAsync(id, ct)).ToDbModel(); + Player dbPlayer = (await _vortex.FetchAccountAsync(id, ct) ?? throw new InvalidOperationException($"Player {id} not found.")).ToDbModel(); players.Add(id, dbPlayer); context.Players.Add(dbPlayer); } diff --git a/WowsKarma.Api/Services/Discord/ModActionWebhookService.cs b/WowsKarma.Api/Services/Discord/ModActionWebhookService.cs index d3c7d898..c4aec209 100644 --- a/WowsKarma.Api/Services/Discord/ModActionWebhookService.cs +++ b/WowsKarma.Api/Services/Discord/ModActionWebhookService.cs @@ -4,11 +4,11 @@ namespace WowsKarma.Api.Services.Discord; -public class ModActionWebhookService : WebhookService +public sealed class ModActionWebhookService : WebhookService { public ModActionWebhookService(IConfiguration configuration) : base(configuration) { - foreach (string webhookLink in configuration.GetSection($"Discord:Webhooks:{Startup.ApiRegion.ToRegionString()}:ModActions").Get()) + foreach (string webhookLink in configuration.GetSection($"Discord:Webhooks:{Startup.ApiRegion.ToRegionString()}:ModActions").Get() ?? []) { Client.AddWebhookAsync(new(webhookLink)).GetAwaiter().GetResult(); } @@ -68,8 +68,8 @@ public async Task SendPlatformBanWebhookAsync(PlatformBan ban) Color = DiscordColor.Red }; - embed.AddField("Banned by", $"[{ban.Mod?.Username ?? "Unknown"}]({ban.Mod.GetPlayerProfileLink()})", true); - embed.AddField("Reason", ban.Reason, false); + embed.AddField("Banned by", $"[{ban.Mod?.Username ?? "Unknown"}]({ban.Mod?.GetPlayerProfileLink()})", true); + embed.AddField("Reason", ban.Reason); if (ban.BannedUntil is not null) { @@ -83,9 +83,9 @@ await Client.BroadcastMessageAsync(GetCurrentRegionWebhookBuilder() private static DiscordEmbedBuilder AddModActionContent(DiscordEmbedBuilder embed, PostModAction modAction) { - embed.AddField("Moderated by", $"[{modAction.Mod?.Username ?? "Unknown"}]({modAction.Mod.GetPlayerProfileLink()})", true); + embed.AddField("Moderated by", $"[{modAction.Mod?.Username ?? "Unknown"}]({modAction.Mod?.GetPlayerProfileLink()})", true); embed.AddField("Post Author", $"[{modAction.Post.Author?.Username ?? "Unknown"}]({modAction.Post.Author?.GetPlayerProfileLink()})", true); - embed.AddField("Reason", modAction.Reason, false); + embed.AddField("Reason", modAction.Reason); return embed; } diff --git a/WowsKarma.Api/Services/Discord/PostWebhookService.cs b/WowsKarma.Api/Services/Discord/PostWebhookService.cs index b50d8d4e..2c2388d5 100644 --- a/WowsKarma.Api/Services/Discord/PostWebhookService.cs +++ b/WowsKarma.Api/Services/Discord/PostWebhookService.cs @@ -3,14 +3,13 @@ using WowsKarma.Common; using WowsKarma.Common.Models.DTOs.Replays; - namespace WowsKarma.Api.Services.Discord; -public class PostWebhookService : WebhookService +public sealed class PostWebhookService : WebhookService { public PostWebhookService(IConfiguration configuration) : base(configuration) { - foreach (string webhookLink in configuration.GetSection($"Discord:Webhooks:{Startup.ApiRegion.ToRegionString()}:Posts").Get()) + foreach (string webhookLink in configuration.GetSection($"Discord:Webhooks:{Startup.ApiRegion.ToRegionString()}:Posts").Get() ?? []) { Client.AddWebhookAsync(new(webhookLink)).GetAwaiter().GetResult(); } @@ -73,7 +72,7 @@ public async Task SendDeletedPostWebhookAsync(PlayerPostDTO post) null or _ => "Neutral" }; - private static DiscordEmbedBuilder AddReplayStatus(DiscordEmbedBuilder embed, ReplayDTO replay) => embed.AddField("Replay", replay is null + private static DiscordEmbedBuilder AddReplayStatus(DiscordEmbedBuilder embed, ReplayDTO? replay) => embed.AddField("Replay", replay is null ? "*No replay provided*" : $"[{replay.Id}]({replay.DownloadUri})" ); @@ -81,7 +80,7 @@ private static DiscordEmbedBuilder AddReplayStatus(DiscordEmbedBuilder embed, Re private static DiscordEmbedBuilder AddPostContent(DiscordEmbedBuilder embed, PlayerPostDTO post) { embed.Description = post.Content; - PostFlairsParsed parsedFlairs = post.Flairs.ParseFlairsEnum(); + PostFlairsParsed? parsedFlairs = post.Flairs.ParseFlairsEnum(); embed.AddField("Performance", GetFlairValueString(parsedFlairs?.Performance), true); embed.AddField("Teamplay", GetFlairValueString(parsedFlairs?.Teamplay), true); diff --git a/WowsKarma.Api/Services/Discord/WebhookService.cs b/WowsKarma.Api/Services/Discord/WebhookService.cs index 412a7f55..49e9398f 100644 --- a/WowsKarma.Api/Services/Discord/WebhookService.cs +++ b/WowsKarma.Api/Services/Discord/WebhookService.cs @@ -8,26 +8,26 @@ public abstract class WebhookService { protected DiscordWebhookClient Client { get; private init; } - private readonly Uri webhookUserAvatarLink; - private readonly string apiRegionString; + private readonly Uri _webhookUserAvatarLink; + private readonly string _apiRegionString; - public WebhookService(IConfiguration configuration) + protected WebhookService(IConfiguration configuration) { Client = new(); - apiRegionString = Startup.ApiRegion.ToRegionString(); - webhookUserAvatarLink = new(configuration["Discord:WebhookAvatarPath"]); + _apiRegionString = Startup.ApiRegion.ToRegionString(); + _webhookUserAvatarLink = new(configuration["Discord:WebhookAvatarPath"] ?? throw new InvalidOperationException("Missing Discord:WebhookAvatarPath in configuration.")); } protected DiscordWebhookBuilder GetCurrentRegionWebhookBuilder() => new() { - AvatarUrl = webhookUserAvatarLink.AbsoluteUri, - Username = $"WOWS Karma ({apiRegionString})" + AvatarUrl = _webhookUserAvatarLink.AbsoluteUri, + Username = $"WOWS Karma ({_apiRegionString})" }; protected DiscordEmbedBuilder.EmbedFooter GetDefaultFooter() => new() { - IconUrl = webhookUserAvatarLink.AbsoluteUri, - Text = $"WOWS Karma ({apiRegionString}) v{Startup.DisplayVersion} - Powered by Nodsoft Systems" + IconUrl = _webhookUserAvatarLink.AbsoluteUri, + Text = $"WOWS Karma ({_apiRegionString}) v{Startup.DisplayVersion} - Powered by Nodsoft Systems" }; } \ No newline at end of file diff --git a/WowsKarma.Api/Services/KarmaService.cs b/WowsKarma.Api/Services/KarmaService.cs index 38f564aa..879aea22 100644 --- a/WowsKarma.Api/Services/KarmaService.cs +++ b/WowsKarma.Api/Services/KarmaService.cs @@ -4,7 +4,7 @@ public class KarmaService { public KarmaService() { } - public static void UpdatePlayerKarma(Player player, PostFlairsParsed newFlairs, PostFlairsParsed oldFlairs, bool allowNegative) + public static void UpdatePlayerKarma(Player player, PostFlairsParsed? newFlairs, PostFlairsParsed? oldFlairs, bool allowNegative) { sbyte? newKarmaBalance = newFlairs is null ? null : PostFlairsUtils.CountBalance(newFlairs); sbyte? oldKarmaBalance = oldFlairs is null ? null : PostFlairsUtils.CountBalance(oldFlairs); @@ -36,7 +36,7 @@ public static void UpdatePlayerKarma(Player player, PostFlairsParsed newFlairs, } } - public static void UpdatePlayerRatings(Player player, PostFlairsParsed postFlairs, PostFlairsParsed oldFlairs) + public static void UpdatePlayerRatings(Player player, PostFlairsParsed? postFlairs, PostFlairsParsed? oldFlairs) { player.PerformanceRating = UpdateRating(player.PerformanceRating, postFlairs?.Performance, oldFlairs?.Performance); player.TeamplayRating = UpdateRating(player.TeamplayRating, postFlairs?.Teamplay, oldFlairs?.Teamplay); diff --git a/WowsKarma.Api/Services/MinimapRenderingService.cs b/WowsKarma.Api/Services/MinimapRenderingService.cs index b864a7c3..8b375fa6 100644 --- a/WowsKarma.Api/Services/MinimapRenderingService.cs +++ b/WowsKarma.Api/Services/MinimapRenderingService.cs @@ -1,11 +1,8 @@ -using System.IO; -using System.Threading; -using Azure.Storage.Blobs; +using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Hangfire; using Hangfire.Tags.Attributes; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using WowsKarma.Api.Data; using WowsKarma.Api.Data.Models.Replays; using WowsKarma.Api.Minimap.Client; @@ -39,7 +36,8 @@ IConfiguration configuration _context = context; _logger = logger; - string connectionString = configuration[$"API:{Startup.ApiRegion.ToRegionString()}:Azure:Storage:ConnectionString"]; + string connectionString = configuration[$"API:{Startup.ApiRegion.ToRegionString()}:Azure:Storage:ConnectionString"] + ?? throw new ArgumentException("Missing API:{region}:Azure:Storage:ConnectionString configuration value."); BlobServiceClient serviceClient = new(connectionString); _containerClient = serviceClient.GetBlobContainerClient(MinimapBlobContainer); diff --git a/WowsKarma.Api/Services/ModService.cs b/WowsKarma.Api/Services/ModService.cs index 1066dcf6..7161caf6 100644 --- a/WowsKarma.Api/Services/ModService.cs +++ b/WowsKarma.Api/Services/ModService.cs @@ -1,17 +1,16 @@ -using Mapster; +using System.Diagnostics; +using Mapster; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.Extensions.Logging; using WowsKarma.Api.Data; using WowsKarma.Api.Data.Models.Notifications; using WowsKarma.Api.Services.Discord; using WowsKarma.Api.Services.Posts; - namespace WowsKarma.Api.Services; -public class ModService +public sealed class ModService { private readonly ILogger _logger; private readonly ModActionWebhookService _webhookService; @@ -28,7 +27,7 @@ public ModService(ILogger logger, ApiDbContext context, ModActionWeb _notifications = notifications; } - public Task GetModActionAsync(Guid id) => _context.PostModActions.AsNoTracking().FirstOrDefaultAsync(ma => ma.Id == id); + public Task GetModActionAsync(Guid id) => _context.PostModActions.AsNoTracking().FirstOrDefaultAsync(ma => ma.Id == id); public IQueryable GetPostModActions(Guid postId) => _context.PostModActions.AsNoTracking() .Include(ma => ma.Post) @@ -40,7 +39,7 @@ public IQueryable GetPostModActions(uint playerId) => _context.Po .Include(ma => ma.Mod) .Where(ma => ma.Post.AuthorId == playerId); - public Task GetPlatformBan(Guid id) => _context.PlatformBans.AsNoTracking().FirstOrDefaultAsync(b => b.Id == id); + public Task GetPlatformBan(Guid id) => _context.PlatformBans.AsNoTracking().FirstOrDefaultAsync(b => b.Id == id); public IQueryable GetPlatformBans(uint userId) => _context.PlatformBans.AsNoTracking().Where(b => b.UserId == userId); @@ -52,11 +51,14 @@ public async Task SubmitPostModActionAsync(PostModActionDTO modAction) switch (modAction.ActionType) { case ModActionType.Deletion: + { await _postService.DeletePostAsync(modAction.PostId, true); await _notifications.SendNewNotification(PostModDeletedNotification.FromModAction(entityEntry.Entity)); break; + } case ModActionType.Update: + { PlayerPostDTO current = _postService.GetPost(modAction.PostId).Adapt(); await _postService.EditPostAsync(modAction.PostId, current with @@ -67,6 +69,10 @@ await _postService.EditPostAsync(modAction.PostId, current with }, true); break; + } + + default: + throw new UnreachableException(); } await entityEntry.Reference(pma => pma.Mod).LoadAsync(); @@ -81,9 +87,8 @@ public async Task RevertModActionAsync(Guid modActionId) _context.PostModActions.Remove(modAction); await _postService.RevertPostModLockAsync(modAction.PostId); - await _context.SaveChangesAsync(); - + await _webhookService.SendModActionRevertedWebhookAsync(modAction); } @@ -126,9 +131,9 @@ await _notifications.SendNewNotification(new PlatformBanNotification public async Task RevertPlatformBanAsync(Guid id) { - PlatformBan ban = await _context.PlatformBans.FindAsync(id); + PlatformBan ban = await _context.PlatformBans.FindAsync(id) ?? throw new ArgumentException($"Platform ban with id {id} not found"); ban.Reverted = true; await _context.SaveChangesAsync(); - _logger.LogInformation("Reverted Ban {BanId}.", ban.Id); + _logger.LogInformation("Reverted Ban {banId}.", ban.Id); } } diff --git a/WowsKarma.Api/Services/NotificationService.cs b/WowsKarma.Api/Services/NotificationService.cs index 397d1c01..dc874cc1 100644 --- a/WowsKarma.Api/Services/NotificationService.cs +++ b/WowsKarma.Api/Services/NotificationService.cs @@ -3,8 +3,6 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.Extensions.Logging; using WowsKarma.Api.Data; using WowsKarma.Api.Data.Models.Notifications; using WowsKarma.Api.Hubs; @@ -13,7 +11,6 @@ namespace WowsKarma.Api.Services; - public class NotificationService { private readonly ILogger _logger; @@ -41,7 +38,7 @@ from n in _context.Set().IncludeAllNotificationsChildNavs() public IQueryable GetNotifications(Guid[] ids) => _context.Set().Where(n => ids.Contains(n.Id)); - public TNotification GetNotification(Guid id) where TNotification : NotificationBase + public TNotification? GetNotification(Guid id) where TNotification : NotificationBase => _context.Set().FirstOrDefault(n => n.Id == id); public async Task SendNewNotification(TNotification notification) where TNotification : NotificationBase @@ -55,7 +52,7 @@ public async Task SendNewNotification(TNotification notification) NotificationBaseDTO dto = entry.Entity.ToDTO(); await _hub.Clients.User(notification.AccountId.ToString()).NewNotification(typeof(TNotification).FullName!, dto); - _logger.LogInformation("Sent notification {NotificationId} to user {UserId}.", notification.Id, notification.AccountId); + _logger.LogInformation("Sent notification {notificationId} to user {userId}.", notification.Id, notification.AccountId); } public async Task AcknowledgeNotifications(Guid[] ids) @@ -72,22 +69,25 @@ public void AcknowledgeNotifications(IEnumerable notifications { if (notifications.Any()) { + List ids = []; + foreach (NotificationBase notification in notifications) { notification.AcknowledgedAt = DateTime.UtcNow; + ids.Add(notification.Id); } _context.SaveChanges(); - _logger.LogInformation("Acknowledged Notifications {NotificationId}.", string.Join(", ", notifications.Select(n => n.Id))); + _logger.LogInformation("Acknowledged Notifications {notificationId}.", string.Join(", ", ids)); } } - public async Task DeleteNotification(Guid id) + public async Task DeleteNotificationAsync(Guid id) { NotificationBase notification = await _context.Set().FindAsync(id) ?? throw new ArgumentException("No notification found for given ID.", nameof(id)); _context.Remove(notification); await _context.SaveChangesAsync(); - _logger.LogInformation("Removed Notification {Id}.", id); + _logger.LogInformation("Removed Notification {id}.", id); await _hub.Clients.All.DeletedNotification(id); } } @@ -97,7 +97,7 @@ public static class NotificationServiceExtensions public static IQueryable IncludeAllNotificationsChildNavs(this IQueryable query) { //PlatformBanNotification - query = query.Include(static n => (n as PlatformBanNotification).Ban); + query = query.Include(static n => (n as PlatformBanNotification)!.Ban); // PostAddedNotification @@ -107,10 +107,10 @@ public static IQueryable IncludeAllNotificationsChildNavs(this query = query.IncludeAllPostNotificationsChildNavs(); // PostModEditedNotification - query = query.Include(static n => (n as PostModEditedNotification).ModAction); + query = query.Include(static n => (n as PostModEditedNotification)!.ModAction); // PostModDeletedNotification - query = query.Include(static n => (n as PostModDeletedNotification).ModAction); + query = query.Include(static n => (n as PostModDeletedNotification)!.ModAction); return query; } @@ -123,10 +123,10 @@ public static IQueryable IncludeAllNotificationsChildNavs(this public static IQueryable IncludeAllPostNotificationsChildNavs(this IQueryable query) where TNotification : PostNotificationBase { - query = query.Include(static n => (n as TNotification).Post) + query = query.Include(static n => (n as TNotification)!.Post) .ThenInclude(static p => p.Author); - query = query.Include(static n => (n as TNotification).Post) + query = query.Include(static n => (n as TNotification)!.Post) .ThenInclude(static p => p.Player); return query; diff --git a/WowsKarma.Api/Services/PlayerService.cs b/WowsKarma.Api/Services/PlayerService.cs index 45df2ffd..b1606f8b 100644 --- a/WowsKarma.Api/Services/PlayerService.cs +++ b/WowsKarma.Api/Services/PlayerService.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using System.Threading; using Hangfire; using Hangfire.Tags.Attributes; using Nodsoft.Wargaming.Api.Client.Clients.Wows; @@ -42,7 +41,7 @@ public PlayerService(ApiDbContext context, WowsPublicApiClient wgApi, WowsVortex [Tag("player", "update", "batch"), JobDisplayName("Perform API fetch on player batch")] public async Task> GetPlayersAsync(IEnumerable ids, bool includeRelated = false, bool includeClanInfo = false, CancellationToken ct = default) { - List players = new(); + List players = []; foreach (uint id in ids.AsParallel().WithCancellation(ct)) { @@ -144,7 +143,7 @@ public async Task> ListPlayersAsync(string search AccountListing[] result = (await _wgApi.ListPlayersAsync(search))?.Data?.ToArray(); return result is { Length: > 0 } - ? result.Select(listing => listing.ToDTO()) + ? result.Select(listing => listing.ToDto()) : null; } @@ -267,7 +266,7 @@ public async Task RecalculatePlayerMetrics(uint playerId, CancellationToken ct) } internal static bool UpdateNeeded(Player player) => player.UpdatedAt + DataUpdateSpan < DateTime.UtcNow; - internal static bool IsOptOutOnCooldown(DateTimeOffset lastChange) => lastChange + OptOutCooldownSpan > DateTimeOffset.UtcNow; + internal static bool IsOptOutOnCooldown(DateTimeOffset? lastChange) => lastChange is not null && lastChange + OptOutCooldownSpan > DateTimeOffset.UtcNow; private static void SetPlayerMetrics(Player player, int site, int performance, int teamplay, int courtesy) { diff --git a/WowsKarma.Api/Services/Posts/PostService.cs b/WowsKarma.Api/Services/Posts/PostService.cs index 8b67770a..d7132d3e 100644 --- a/WowsKarma.Api/Services/Posts/PostService.cs +++ b/WowsKarma.Api/Services/Posts/PostService.cs @@ -1,17 +1,14 @@ -using System.Threading; using Mapster; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using WowsKarma.Api.Data; -using WowsKarma.Api.Data.Models.Notifications; using WowsKarma.Api.Data.Models.Replays; using WowsKarma.Api.Infrastructure.Exceptions; using WowsKarma.Api.Services.Replays; namespace WowsKarma.Api.Services.Posts; -public class PostService +public sealed class PostService { public const ushort PostTitleMaxSize = 60; public const ushort PostContentMaxSize = 2000; @@ -38,10 +35,12 @@ public PostService(ApiDbContext context, PlayerService playerService, ReplaysIng ); /// - /// Gets a post by id. + /// Gets a post by its ID. /// - public Post GetPost(Guid id) => GetPost(_context, id); - internal static Post GetPost(ApiDbContext context, Guid id) => context.Posts + /// The post's ID. + /// The post, or if not found. + public Post? GetPost(Guid id) => GetPost(_context, id); + internal static Post? GetPost(ApiDbContext context, Guid id) => context.Posts .Include(p => p.Author) .ThenInclude(p => p.ClanMember) .ThenInclude(p => p.Clan) @@ -53,17 +52,23 @@ internal static Post GetPost(ApiDbContext context, Guid id) => context.Posts .Include(p => p.Replay) .FirstOrDefault(p => p.Id == id); - public async Task GetPostDTOAsync(Guid id) + /// + /// Gets a post's DTO by its ID. + /// + /// The post's ID. + /// The post, or if not found. + public async Task GetPostDTOAsync(Guid id) { - Post post = GetPost(id); - PlayerPostDTO postDTO = post?.Adapt(); + if (GetPost(id) is not { } post) + { + return null; + } + + PlayerPostDTO postDto = post.Adapt(); - return post?.ReplayId is null - ? postDTO - : postDTO with - { - Replay = await _replayService.GetReplayDTOAsync(post.ReplayId.Value) - }; + return post.ReplayId is null + ? postDto + : postDto with { Replay = await _replayService.GetReplayDTOAsync(post.ReplayId.Value) }; } public IQueryable GetReceivedPosts(uint playerId) => _context.Posts.AsNoTracking() @@ -87,7 +92,7 @@ public IQueryable GetSentPosts(uint authorId) => _context.Posts.AsNoTracki .ThenInclude(p => p.ClanMember) .ThenInclude(p => p.Clan) - .Where(p => p.AuthorId == authorId)? + .Where(p => p.AuthorId == authorId) .OrderByDescending(p => p.CreatedAt); public IQueryable GetLatestPosts() => _context.Posts.AsNoTracking() @@ -101,11 +106,11 @@ public IQueryable GetLatestPosts() => _context.Posts.AsNoTracking() .OrderByDescending(p => p.CreatedAt); - public async Task CreatePostAsync(PlayerPostDTO postDto, IFormFile replayFile, bool bypassChecks) + public async Task CreatePostAsync(PlayerPostDTO postDto, IFormFile? replayFile, bool bypassChecks) { bool hasReplay = replayFile is not null; - Task replayIngestTask = hasReplay ? _replayService.IngestReplayAsync(replayFile, CancellationToken.None) : null; + Task? replayIngestTask = hasReplay ? _replayService.IngestReplayAsync(replayFile, CancellationToken.None) : null; try { @@ -141,7 +146,7 @@ public async Task CreatePostAsync(PlayerPostDTO postDto, IFormFile replayF if (hasReplay) { - Replay replay = await replayIngestTask; + Replay replay = await replayIngestTask!; entry.Entity.ReplayId = replay.Id; entry.Entity.Replay = replay; @@ -181,7 +186,7 @@ public async Task EditPostAsync(Guid id, PlayerPostDTO edited, bool modEditLock public async Task DeletePostAsync(Guid id, bool modLock = false) { Post post = await _context.Posts.FindAsync(id) ?? throw new ArgumentException($"Post {id} not found", nameof(id)); - Player player = await _context.Players.FindAsync(post.PlayerId)!; + Player player = await _context.Players.FindAsync(post.PlayerId) ?? throw new ArgumentException($"Player Account {post.PlayerId} not found", nameof(id)); if (modLock) { @@ -235,7 +240,7 @@ from p in _context.Posts if (filteredPosts.Any()) { - PlayerPostDTO lastAuthoredPost = filteredPosts.OrderBy(p => p.CreatedAt).LastOrDefault()?.Adapt(); + PlayerPostDTO? lastAuthoredPost = filteredPosts.OrderBy(p => p.CreatedAt).LastOrDefault()?.Adapt(); if (lastAuthoredPost is { CreatedAt: not null }) { @@ -244,7 +249,7 @@ from p in _context.Posts } } - + return false; } } \ No newline at end of file diff --git a/WowsKarma.Api/Services/Posts/PostUpdatesBroadcastService.cs b/WowsKarma.Api/Services/Posts/PostUpdatesBroadcastService.cs index 22611efc..7f064547 100644 --- a/WowsKarma.Api/Services/Posts/PostUpdatesBroadcastService.cs +++ b/WowsKarma.Api/Services/Posts/PostUpdatesBroadcastService.cs @@ -1,5 +1,4 @@ -using System.Threading; -using Hangfire; +using Hangfire; using Hangfire.Tags.Attributes; using Mapster; using Microsoft.AspNetCore.SignalR; @@ -10,7 +9,6 @@ using WowsKarma.Common.Hubs; -#nullable enable namespace WowsKarma.Api.Services.Posts; // ReSharper disable MemberCanBePrivate.Global @@ -82,7 +80,7 @@ public static void OnPostDeletionAsync(PlayerPostDTO post, bool modlock) public async Task LogPostCreationAsync(Guid postId) { // Get the post from the database, and adapt to DTO. - Post post = PostService.GetPost(_dbContext, postId); + Post post = PostService.GetPost(_dbContext, postId) ?? throw new InvalidOperationException($"Post {postId} not found."); PlayerPostDTO postDto = post.Adapt(); // Send the webhook. @@ -93,7 +91,7 @@ public async Task LogPostCreationAsync(Guid postId) public async Task BroadcastPostCreationAsync(Guid postId) { // Get the post from the database, and adapt to DTO. - Post post = PostService.GetPost(_dbContext, postId); + Post post = PostService.GetPost(_dbContext, postId) ?? throw new InvalidOperationException($"Post {postId} not found."); PlayerPostDTO postDto = post.Adapt(); // Send the update to the clients. @@ -104,7 +102,7 @@ public async Task BroadcastPostCreationAsync(Guid postId) public async Task NotifyPostCreationAsync(Guid postId) { // Get the post from the database, and adapt to DTO. - Post post = PostService.GetPost(_dbContext, postId); + Post post = PostService.GetPost(_dbContext, postId) ?? throw new InvalidOperationException($"Post {postId} not found."); // Send the notification. await _notificationService.SendNewNotification(new PostAddedNotification @@ -122,7 +120,7 @@ await _notificationService.SendNewNotification(new PostAddedNotification public async Task LogPostEditionAsync(Guid postId) { // Get the post from the database, and adapt to DTO. - Post post = PostService.GetPost(_dbContext, postId); + Post post = PostService.GetPost(_dbContext, postId) ?? throw new InvalidOperationException($"Post {postId} not found."); PlayerPostDTO postDto = post.Adapt(); // Send the webhook. @@ -133,7 +131,7 @@ public async Task LogPostEditionAsync(Guid postId) public async Task BroadcastPostEditionAsync(Guid postId) { // Get the post from the database, and adapt to DTO. - Post post = PostService.GetPost(_dbContext, postId); + Post post = PostService.GetPost(_dbContext, postId) ?? throw new InvalidOperationException($"Post {postId} not found."); PlayerPostDTO postDto = post.Adapt(); // Send the update to the clients. @@ -144,7 +142,7 @@ public async Task BroadcastPostEditionAsync(Guid postId) public async Task NotifyPostEditionAsync(Guid postId) { // Get the post from the database, and adapt to DTO. - Post post = PostService.GetPost(_dbContext, postId); + Post post = PostService.GetPost(_dbContext, postId) ?? throw new InvalidOperationException($"Post {postId} not found."); // Send the notification. await _notificationService.SendNewNotification(new PostEditedNotification diff --git a/WowsKarma.Api/Services/Replays/ReplaysIngestService.cs b/WowsKarma.Api/Services/Replays/ReplaysIngestService.cs index dcd30dae..6900cb99 100644 --- a/WowsKarma.Api/Services/Replays/ReplaysIngestService.cs +++ b/WowsKarma.Api/Services/Replays/ReplaysIngestService.cs @@ -1,12 +1,8 @@ using Azure.Storage.Blobs; using Mapster; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.Extensions.Logging; -using System.IO; using System.Security; -using System.Threading; using Hangfire; using Hangfire.Tags.Attributes; using WowsKarma.Api.Data; @@ -17,7 +13,7 @@ namespace WowsKarma.Api.Services.Replays; -public class ReplaysIngestService +public sealed class ReplaysIngestService { public const string ReplayBlobContainer = "replays"; public const string SecurityBlobContainer = "rce-replays"; @@ -32,7 +28,9 @@ public class ReplaysIngestService public ReplaysIngestService(ILogger logger, IConfiguration configuration, ApiDbContext context, ReplaysProcessService processService) { - string connectionString = configuration[$"API:{Startup.ApiRegion.ToRegionString()}:Azure:Storage:ConnectionString"]; + string connectionString = configuration[$"API:{Startup.ApiRegion.ToRegionString()}:Azure:Storage:ConnectionString"] + ?? throw new InvalidOperationException("Missing API:Azure:Storage:ConnectionString in configuration."); + _serviceClient = new(connectionString); _containerClient = _serviceClient.GetBlobContainerClient(ReplayBlobContainer); _securityContainerClient = _serviceClient.GetBlobContainerClient(SecurityBlobContainer); @@ -49,11 +47,19 @@ public ReplaysIngestService(ILogger logger, IConfiguration private static readonly Func> _listReplaysAsync = EF.CompileAsyncQuery( (ApiDbContext context) => context.Replays.Select(r => r.Id)); - public Replay GetReplay(Guid id) => _context.Replays.Find(id); + /// + /// Gets a replay by its ID. + /// + /// The replay's ID. + /// The replay, or if not found. + public Replay? GetReplay(Guid id) => _context.Replays.Find(id); - public async Task GetReplayDTOAsync(Guid id) + public async Task GetReplayDTOAsync(Guid id) { - Replay replay = await _context.Replays.FindAsync(id); + if (await _context.Replays.FindAsync(id) is not { } replay) + { + return null; + } return new() { @@ -75,7 +81,8 @@ public async Task IngestReplayAsync(Guid postId, IFormFile replayFile, C throw new ArgumentOutOfRangeException(nameof(replayFile)); } - Post post = await _context.Posts.FindAsync(new object[] { postId }, cancellationToken: ct); + Post post = await _context.Posts.FindAsync([postId], cancellationToken: ct) + ?? throw new ArgumentException("No post was found for specified GUID.", nameof(postId)); Replay replay = await _processService.ProcessReplayAsync(new Replay(), replayFile.OpenReadStream(), ct); @@ -83,7 +90,7 @@ public async Task IngestReplayAsync(Guid postId, IFormFile replayFile, C if (post.ReplayId is { } existingReplayId) { - await RemoveReplayAsync(GetReplay(existingReplayId)); + await RemoveReplayAsync(GetReplay(existingReplayId) ?? throw new InvalidOperationException("Post has a replay ID, but no replay was found.")); } EntityEntry entityEntry = _context.Replays.Add(replay with { PostId = postId }); @@ -124,7 +131,7 @@ internal async Task IngestRceFileAsync(IFormFile replayFile) { string blobName = $"{Guid.NewGuid():N}-{replayFile.FileName}"; await _securityContainerClient.UploadBlobAsync(blobName, replayFile.OpenReadStream()); - _logger.LogInformation("Ingested RCE file {BlobName}. Link: {Uri}", blobName, _securityContainerClient.GetBlobClient(blobName).Uri); + _logger.LogInformation("Ingested RCE file {blobName}. Link: {uri}", blobName, _securityContainerClient.GetBlobClient(blobName).Uri); } public async Task FetchReplayFileAsync(Guid replayId, CancellationToken ct) @@ -179,11 +186,11 @@ public async Task RemoveReplayAsync(Replay replay) // Catch any CVE-2022-31265 related exceptions and log them. catch (InvalidReplayException e) when (e.InnerException is SecurityException se && se.Data["exploit"] is "CVE-2022-31265") { - _logger.LogWarning("CVE-2022-31265 exploit detected in replay {ReplayId}. Please delete both post and replay from the platform at once.", replay.Id); + _logger.LogWarning("CVE-2022-31265 exploit detected in replay {replayId}. Please delete both post and replay from the platform at once.", replay.Id); } catch (Exception e) { - _logger.LogWarning(e, "Failed to reprocess replay {ReplayId}.", replay.Id); + _logger.LogWarning(e, "Failed to reprocess replay {replayId}.", replay.Id); } return null; @@ -217,7 +224,7 @@ public async Task ReprocessReplayAsync(Guid replayId, CancellationToken ct) [JobDisplayName("Reprocess all replays within date range"), Tag("replay", "recalculation", "batch")] public async Task ReprocessAllReplaysAsync(DateTime? start, DateTime? end, CancellationToken ct) { - _logger.LogWarning("Started reprocessing all replays between {Start:g} and {End:g}", start, end); + _logger.LogWarning("Started reprocessing all replays between {start:g} and {end:g}", start, end); var replayStubs = await _context.Posts.Include(static p => p.Replay) .Where(r => r.Replay != null && r.CreatedAt >= start && r.CreatedAt <= end) @@ -229,9 +236,9 @@ public async Task ReprocessAllReplaysAsync(DateTime? start, DateTime? end, Cance }) .ToArrayAsync(ct); - _logger.LogWarning("Database readout complete. {Count} replays will be reprocessed.", replayStubs.Length); + _logger.LogWarning("Database readout complete. {count} replays will be reprocessed.", replayStubs.Length); - List replays = new(); + List replays = []; foreach (Replay replay in replayStubs) { @@ -241,11 +248,11 @@ public async Task ReprocessAllReplaysAsync(DateTime? start, DateTime? end, Cance } } - _logger.LogWarning("Finished file reprocessing of {Count} replays. Saving to database...", replayStubs.Length); + _logger.LogWarning("Finished file reprocessing of {count} replays. Saving to database...", replayStubs.Length); _context.UpdateRange(replays); await _context.SaveChangesAsync(ct); - _logger.LogWarning("Replay Files reprocessing complete! Reprocessed {Count} replays total.", replayStubs.Length); + _logger.LogWarning("Replay Files reprocessing complete! Reprocessed {count} replays total.", replayStubs.Length); } } diff --git a/WowsKarma.Api/Services/Replays/ReplaysProcessService.cs b/WowsKarma.Api/Services/Replays/ReplaysProcessService.cs index f86a1db7..7a1c44a5 100644 --- a/WowsKarma.Api/Services/Replays/ReplaysProcessService.cs +++ b/WowsKarma.Api/Services/Replays/ReplaysProcessService.cs @@ -1,8 +1,6 @@ using Mapster; -using System.IO; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading; using Nodsoft.WowsReplaysUnpack.ExtendedData; using Nodsoft.WowsReplaysUnpack.ExtendedData.Models; using Nodsoft.WowsReplaysUnpack.Services; @@ -13,10 +11,9 @@ using ReplayPlayerRaw = Nodsoft.WowsReplaysUnpack.ExtendedData.Models.ReplayPlayer; - namespace WowsKarma.Api.Services.Replays; -public class ReplaysProcessService +public sealed class ReplaysProcessService { public static JsonSerializerOptions SerializerOptions { get; } = new() { @@ -37,7 +34,7 @@ public ReplaysProcessService(ReplayUnpackerFactory replayUnpacker, ApiDbContext public async Task ProcessReplayAsync(Guid replayId, Stream replayStream, CancellationToken ct) { - Replay replay = await _context.Replays.FindAsync(new object[] { replayId }, cancellationToken: ct) + Replay replay = await _context.Replays.FindAsync([replayId], cancellationToken: ct) ?? throw new ArgumentException("No replay was found for specified GUID.", nameof(replayId)); await ProcessReplayAsync(replay, replayStream, ct); diff --git a/WowsKarma.Api/Startup.cs b/WowsKarma.Api/Startup.cs index 5f0e1e3e..8a6dcd78 100644 --- a/WowsKarma.Api/Startup.cs +++ b/WowsKarma.Api/Startup.cs @@ -43,7 +43,7 @@ namespace WowsKarma.Api; public sealed class Startup { public static Region ApiRegion { get; private set; } - public static string DisplayVersion { get; private set; } + public static string DisplayVersion { get; private set; } = "0.0.0"; public IConfiguration Configuration { get; } @@ -51,7 +51,7 @@ public Startup(IConfiguration configuration) { Configuration = configuration; ApiRegion = Common.Utilities.GetRegionConfigString(Configuration["Api:CurrentRegion"] ?? "EU"); - DisplayVersion = typeof(Startup).Assembly.GetCustomAttribute()?.InformationalVersion; + DisplayVersion = typeof(Startup).Assembly.GetCustomAttribute()!.InformationalVersion; } @@ -302,7 +302,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) builder.AllowCredentials(); }); - IPAddress[] allowedProxies = Configuration.GetSection("AllowedProxies").Get()?.Select(IPAddress.Parse).ToArray(); + IPAddress[] allowedProxies = Configuration.GetSection("AllowedProxies").Get()?.Select(IPAddress.Parse).ToArray() ?? []; // Nginx configuration step ForwardedHeadersOptions forwardedHeadersOptions = new() diff --git a/WowsKarma.Api/Utilities/Conversions.cs b/WowsKarma.Api/Utilities/Conversions.cs index 16bc1fdd..edf1928e 100644 --- a/WowsKarma.Api/Utilities/Conversions.cs +++ b/WowsKarma.Api/Utilities/Conversions.cs @@ -1,5 +1,6 @@ using System.Drawing; using System.Linq.Expressions; +using Hangfire.Annotations; using Mapster; using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows.Public; using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows.Vortex; @@ -10,6 +11,7 @@ namespace WowsKarma.Api.Utilities; public static class Conversions { + [UsedImplicitly] public static void ConfigureMapping() { TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileWithDebugInfo(); @@ -88,7 +90,8 @@ public static void ConfigureMapping() TypeAdapterConfig.NewConfig().MapWith(x => x == null ? DateTimeOffset.UnixEpoch : new(x.Value.ToDateTime(TimeOnly.MinValue), TimeSpan.Zero)); } - public static AccountListingDTO ToDTO(this AccountListing accountListing) => new(accountListing.AccountId, accountListing.Nickname); + [Pure] + public static AccountListingDTO ToDto(this AccountListing accountListing) => new(accountListing.AccountId, accountListing.Nickname); public static Player ToDbModel(this VortexAccountInfo accountInfo) { @@ -108,8 +111,4 @@ public static Player ToDbModel(this VortexAccountInfo accountInfo) LastBattleTime = DateTime.UnixEpoch.AddSeconds(accountInfo.Statistics.Basic!.LastBattleTime) }; } - - public static Player[] ToDbModel(this VortexAccountInfo[] accountInfos) => Array.ConvertAll(accountInfos, ToDbModel); - - public static int ToInt(this PostFlairs input) => (int)input; } \ No newline at end of file diff --git a/WowsKarma.Api/Utilities/HttpExtensions.cs b/WowsKarma.Api/Utilities/HttpExtensions.cs index ea408dda..e983cfe5 100644 --- a/WowsKarma.Api/Utilities/HttpExtensions.cs +++ b/WowsKarma.Api/Utilities/HttpExtensions.cs @@ -1,13 +1,8 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; -using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Cors.Infrastructure; using WowsKarma.Api.Infrastructure.Data; namespace WowsKarma.Api.Utilities; -#nullable enable - /// /// Provides HTTP extensions for request/response interaction. /// @@ -18,7 +13,6 @@ public static class HttpExtensions /// /// The response to add the headers to. /// The page metadata. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddPaginationHeaders(this HttpResponse response, PageMeta pageMeta) { response.Headers.Append("Content-Page-Current", pageMeta.CurrentPage.ToString()); @@ -31,7 +25,6 @@ public static void AddPaginationHeaders(this HttpResponse response, PageMeta pag /// Sets up CORS configuration for pagination headers. /// /// The builder to add the configuration to. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WithExposedPaginationHeaders(this CorsPolicyBuilder builder) { builder.WithExposedHeaders("Content-Page-Current", "Content-Page-Size", "Content-Page-Total", "Content-Items-Total"); diff --git a/WowsKarma.Api/Utilities/Links.cs b/WowsKarma.Api/Utilities/Links.cs index 647beb1f..fd06f5d6 100644 --- a/WowsKarma.Api/Utilities/Links.cs +++ b/WowsKarma.Api/Utilities/Links.cs @@ -1,9 +1,8 @@ -using WowsKarma.Common; +using JetBrains.Annotations; +using WowsKarma.Common; -#nullable enable namespace WowsKarma.Api.Utilities; - /// /// Provides web link utilities. /// @@ -14,21 +13,26 @@ public static class Links /// /// The player. /// The player's profile link + [Pure] public static string GetPlayerProfileLink(this Player player) => $"{Startup.ApiRegion.GetRegionWebDomain()}player/{player.Id},{player.Username}"; - + /// + [Pure] public static string GetPlayerProfileLink(this AccountListingDTO player) => $"{Startup.ApiRegion.GetRegionWebDomain()}player/{player.Id},{player.Username}"; - + /// + [Pure] public static string GetPlayerProfileLink(this PlayerProfileDTO player) => $"{Startup.ApiRegion.GetRegionWebDomain()}player/{player.Id},{player.Username}"; - + /// /// Gets a player post's link on the web app. /// /// The post. /// The post's link + [Pure] public static string GetPostLink(this Post post) => $"{Startup.ApiRegion.GetRegionWebDomain()}posts/view/{post.Id}"; - + /// + [Pure] public static string GetPostLink(this PlayerPostDTO post) => $"{Startup.ApiRegion.GetRegionWebDomain()}posts/view/{post.Id}"; } \ No newline at end of file diff --git a/WowsKarma.Api/Utilities/LinqExtensions.cs b/WowsKarma.Api/Utilities/LinqExtensions.cs index 96dc8911..aebe1cb0 100644 --- a/WowsKarma.Api/Utilities/LinqExtensions.cs +++ b/WowsKarma.Api/Utilities/LinqExtensions.cs @@ -1,5 +1,4 @@ -using System.Runtime.CompilerServices; -using WowsKarma.Api.Infrastructure.Data; +using WowsKarma.Api.Infrastructure.Data; namespace WowsKarma.Api.Utilities; diff --git a/WowsKarma.Api/Utilities/Reflection.cs b/WowsKarma.Api/Utilities/Reflection.cs index 76c16d81..7d87e915 100644 --- a/WowsKarma.Api/Utilities/Reflection.cs +++ b/WowsKarma.Api/Utilities/Reflection.cs @@ -1,6 +1,9 @@ -namespace WowsKarma.Api.Utilities; +using JetBrains.Annotations; + +namespace WowsKarma.Api.Utilities; public static class Reflection { + [Pure] public static bool ImplementsInterface(this Type type, Type interfaceType) => type.GetInterfaces().Any(t => t == interfaceType); } diff --git a/WowsKarma.Api/WowsKarma.Api.csproj b/WowsKarma.Api/WowsKarma.Api.csproj index d3494a60..0e37e4e5 100644 --- a/WowsKarma.Api/WowsKarma.Api.csproj +++ b/WowsKarma.Api/WowsKarma.Api.csproj @@ -3,6 +3,7 @@ net8.0 preview + enable enable 0.17.2 diff --git a/WowsKarma.Common/Models/DTOs/PlayerProfileDTO.cs b/WowsKarma.Common/Models/DTOs/PlayerProfileDTO.cs index 8bb81e2e..de68d91c 100644 --- a/WowsKarma.Common/Models/DTOs/PlayerProfileDTO.cs +++ b/WowsKarma.Common/Models/DTOs/PlayerProfileDTO.cs @@ -20,7 +20,7 @@ public record PlayerProfileDTO public DateTimeOffset WgAccountCreatedAt { get; init; } public DateTimeOffset LastBattleTime { get; init; } - public DateTimeOffset OptOutChanged { get; init; } + public DateTimeOffset? OptOutChanged { get; init; } public bool NegativeKarmaAble { get; init; } public bool PostsBanned { get; init; } diff --git a/WowsKarma.Common/Models/PostFlairs.cs b/WowsKarma.Common/Models/PostFlairs.cs index 3a1787e6..9690755b 100644 --- a/WowsKarma.Common/Models/PostFlairs.cs +++ b/WowsKarma.Common/Models/PostFlairs.cs @@ -44,10 +44,15 @@ public static PostFlairs SanitizeFlairs(this PostFlairs flairs) Courtesy = ParseBalancedFlags(flairs, PostFlairs.CourtesyGood, PostFlairs.CourtesyBad) }; - public static PostFlairs ToEnum(this PostFlairsParsed flairsParsed) + public static PostFlairs ToEnum(this PostFlairsParsed? flairsParsed) { int flairCount = 0x00; + if (flairsParsed is null) + { + return PostFlairs.Neutral; + } + flairCount += flairsParsed.Performance is null ? 0x00 : flairsParsed.Performance.Value ? 0x01 : 0x02; flairCount += flairsParsed.Teamplay is null ? 0x00 : flairsParsed.Teamplay.Value ? 0x04 : 0x08; flairCount += flairsParsed.Courtesy is null ? 0x00 : flairsParsed.Courtesy.Value ? 0x10 : 0x20; diff --git a/WowsKarma.Common/Utilities.cs b/WowsKarma.Common/Utilities.cs index 9fb2c85e..81a4c459 100644 --- a/WowsKarma.Common/Utilities.cs +++ b/WowsKarma.Common/Utilities.cs @@ -3,11 +3,11 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using JetBrains.Annotations; using Nodsoft.Wargaming.Api.Common; using WowsKarma.Common.Models; using WowsKarma.Common.Models.DTOs; -#nullable enable namespace WowsKarma.Common; @@ -23,7 +23,8 @@ public static class Utilities { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - + + [Pure] public static Region GetRegionConfigString(string configString) => configString switch { "EU" => Region.EU, @@ -33,6 +34,7 @@ public static class Utilities _ => throw new ArgumentOutOfRangeException(nameof(configString)) }; + [Pure] public static string ToRegionString(this Region region) => region switch { Region.EU => "EU", @@ -42,6 +44,7 @@ public static class Utilities _ => throw new ArgumentOutOfRangeException(nameof(region)) }; + [Pure] public static string ToWargamingSubdomain(this Region region) => region switch { Region.EU => "eu", @@ -51,6 +54,7 @@ public static class Utilities _ => throw new ArgumentOutOfRangeException(nameof(region)) }; + [Pure] public static Region FromWargamingSubdomain(this string? subdomain) => subdomain switch { "eu" => Region.EU, @@ -60,6 +64,7 @@ public static class Utilities _ => throw new ArgumentOutOfRangeException(nameof(subdomain)) }; + [Pure] public static string GetRegionWebDomain(this Region region) => region switch { Region.EU => "https://wows-karma.com/", @@ -69,6 +74,7 @@ public static class Utilities _ => throw new ArgumentOutOfRangeException(nameof(region)) }; + [Pure] public static string GetRegionApiDomain(this Region region) => region switch { Region.EU => "https://api.wows-karma.com/", @@ -78,6 +84,7 @@ public static class Utilities _ => throw new ArgumentOutOfRangeException(nameof(region)) }; + [Pure] public static string BuildQuery(params (string parameter, string value)[] arguments) { StringBuilder path = new(); @@ -90,27 +97,31 @@ public static string BuildQuery(params (string parameter, string value)[] argume return path.ToString(); } - public static string BuildQuery(this IDictionary arguments) + [Pure] + public static string BuildQuery(this IDictionary arguments) { - using IEnumerator> enumerator = arguments.GetEnumerator(); + using IEnumerator> enumerator = arguments.GetEnumerator(); StringBuilder path = new(); for (int i = 0; i < arguments.Count; i++) { enumerator.MoveNext(); - KeyValuePair current = enumerator.Current; - path.Append($"{(i is 0 ? '?' : '&')}{current.Key}={Uri.EscapeDataString(current.Value)}"); + if (enumerator.Current is (var key, { } value)) + { + path.Append($"{(i is 0 ? '?' : '&')}{key}={Uri.EscapeDataString(value)}"); + } } return path.ToString(); } - + public static AccountListingDTO? ToAccountListing(this ClaimsPrincipal? claimsPrincipal) => uint.TryParse(claimsPrincipal?.FindFirst(ClaimTypes.NameIdentifier)?.Value, out uint accountId) ? new AccountListingDTO(accountId, claimsPrincipal.FindFirst(ClaimTypes.Name)!.Value) : null; + [Pure] public static Type? GetType(string typeName) { if (Type.GetType(typeName) is { } type) @@ -129,6 +140,7 @@ public static string BuildQuery(this IDictionary arguments) return null; } + [Pure] public static ReplayChatMessageChannel GetMessageChannelType(string messageGroup) => messageGroup switch { "battle_common" => ReplayChatMessageChannel.All, @@ -137,6 +149,7 @@ public static string BuildQuery(this IDictionary arguments) _ => ReplayChatMessageChannel.Unknown }; + [Pure] public static string GetDisplayString(this ReplayChatMessageChannel channel) => channel switch { ReplayChatMessageChannel.All => "All", From 1ece247d643512c00b250d8af337fae5f357f04c Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sun, 14 Jan 2024 13:05:37 +0100 Subject: [PATCH 14/20] feat(migrations): Add Nullable context migrations This commit adds nullable context migrations to the codebase. The `UpdateNullableContext` migration updates several columns in different tables to be non-nullable. Specifically, it modifies the `Players`, `ChatMessages`, and `BlobName` columns in the `Replays` table, the `Reason` column in the `PostModActions` table, and the `Username`, `OptOutChanged`, `Tag`, `Name`, and `Description` columns in the `Clans` table. These changes ensure that these columns cannot have null values. --- ...14120317_UpdateNullableContext.Designer.cs | 599 ++++++++++++++++++ .../20240114120317_UpdateNullableContext.cs | 199 ++++++ .../ApiDb/ApiDbContextModelSnapshot.cs | 14 +- 3 files changed, 810 insertions(+), 2 deletions(-) create mode 100644 WowsKarma.Api/Migrations/ApiDb/20240114120317_UpdateNullableContext.Designer.cs create mode 100644 WowsKarma.Api/Migrations/ApiDb/20240114120317_UpdateNullableContext.cs diff --git a/WowsKarma.Api/Migrations/ApiDb/20240114120317_UpdateNullableContext.Designer.cs b/WowsKarma.Api/Migrations/ApiDb/20240114120317_UpdateNullableContext.Designer.cs new file mode 100644 index 00000000..112cc4e8 --- /dev/null +++ b/WowsKarma.Api/Migrations/ApiDb/20240114120317_UpdateNullableContext.Designer.cs @@ -0,0 +1,599 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows; +using Nodsoft.WowsReplaysUnpack.Core.Models; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WowsKarma.Api.Data; +using WowsKarma.Api.Data.Models.Replays; +using WowsKarma.Common.Models; + +#nullable disable + +namespace WowsKarma.Api.Migrations.ApiDb +{ + [DbContext(typeof(ApiDbContext))] + [Migration("20240114120317_UpdateNullableContext")] + partial class UpdateNullableContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "clan_role", new[] { "unknown", "commander", "executive_officer", "recruitment_officer", "commissioned_officer", "officer", "private" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "mod_action_type", new[] { "deletion", "update" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "notification_type", new[] { "unknown", "other", "post_added", "post_edited", "post_deleted", "post_mod_edited", "post_mod_deleted", "platform_ban" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Clan", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDisbanded") + .HasColumnType("boolean"); + + b.Property("LeagueColor") + .HasColumnType("bigint"); + + b.Property("MembersUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Clans"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.ClanMember", b => + { + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ClanId") + .HasColumnType("bigint"); + + b.Property("JoinedAt") + .HasColumnType("date"); + + b.Property("LeftAt") + .HasColumnType("date"); + + b.Property("Role") + .HasColumnType("clan_role"); + + b.HasKey("PlayerId"); + + b.HasIndex("ClanId"); + + b.ToTable("ClanMembers"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.NotificationBase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("AcknowledgedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("notification_type"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("Notifications"); + + b.HasDiscriminator("Type").IsComplete(false).HasValue(NotificationType.Unknown); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.PlatformBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BannedUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ModId") + .HasColumnType("bigint"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reverted") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ModId"); + + b.HasIndex("UserId"); + + b.ToTable("PlatformBans"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Player", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CourtesyRating") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("GameKarma") + .HasColumnType("integer"); + + b.Property("LastBattleTime") + .HasColumnType("timestamp with time zone"); + + b.Property("OptOutChanged") + .HasColumnType("timestamp with time zone"); + + b.Property("OptedOut") + .HasColumnType("boolean"); + + b.Property("PerformanceRating") + .HasColumnType("integer"); + + b.Property("PostsBanned") + .HasColumnType("boolean"); + + b.Property("SiteKarma") + .HasColumnType("integer"); + + b.Property("TeamplayRating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.Property("WgAccountCreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WgHidden") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Flairs") + .HasColumnType("integer"); + + b.Property("ModLocked") + .HasColumnType("boolean"); + + b.Property("NegativeKarmaAble") + .HasColumnType("boolean"); + + b.Property("PlayerId") + .HasColumnType("bigint"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ReplayId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PlayerId"); + + b.HasIndex("ReplayId") + .IsUnique(); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.PostModAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasColumnType("mod_action_type"); + + b.Property("ModId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ModId"); + + b.HasIndex("PostId"); + + b.ToTable("PostModActions"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Replays.Replay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ArenaInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("BlobName") + .IsRequired() + .HasColumnType("text"); + + b.Property>("ChatMessages") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("MinimapRendered") + .HasColumnType("boolean"); + + b.Property>("Players") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PostId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Replays"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PlatformBanNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("BanId") + .HasColumnType("uuid"); + + b.HasIndex("BanId"); + + b.HasDiscriminator().HasValue(NotificationType.PlatformBan); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostAddedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("PostId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid"); + + b.HasIndex("PostId"); + + b.HasDiscriminator().HasValue(NotificationType.PostAdded); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostDeletedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("PostId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid"); + + b.HasIndex("PostId"); + + b.HasDiscriminator().HasValue(NotificationType.PostDeleted); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostEditedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("PostId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid"); + + b.HasIndex("PostId"); + + b.HasDiscriminator().HasValue(NotificationType.PostEdited); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostModDeletedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("ModActionId") + .HasColumnType("uuid"); + + b.HasIndex("ModActionId"); + + b.HasDiscriminator().HasValue(NotificationType.PostModDeleted); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostModEditedNotification", b => + { + b.HasBaseType("WowsKarma.Api.Data.Models.Notifications.NotificationBase"); + + b.Property("ModActionId") + .HasColumnType("uuid"); + + b.HasIndex("ModActionId"); + + b.ToTable("Notifications", t => + { + t.Property("ModActionId") + .HasColumnName("PostModEditedNotification_ModActionId"); + }); + + b.HasDiscriminator().HasValue(NotificationType.PostModEdited); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.ClanMember", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Clan", "Clan") + .WithMany("Members") + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WowsKarma.Api.Data.Models.Player", "Player") + .WithOne("ClanMember") + .HasForeignKey("WowsKarma.Api.Data.Models.ClanMember", "PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Clan"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.NotificationBase", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Player", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.PlatformBan", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Player", "Mod") + .WithMany() + .HasForeignKey("ModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WowsKarma.Api.Data.Models.Player", "User") + .WithMany("PlatformBans") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Mod"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Post", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Player", "Author") + .WithMany("PostsSent") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("WowsKarma.Api.Data.Models.Player", "Player") + .WithMany("PostsReceived") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("WowsKarma.Api.Data.Models.Replays.Replay", "Replay") + .WithOne("Post") + .HasForeignKey("WowsKarma.Api.Data.Models.Post", "ReplayId"); + + b.Navigation("Author"); + + b.Navigation("Player"); + + b.Navigation("Replay"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.PostModAction", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Player", "Mod") + .WithMany() + .HasForeignKey("ModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WowsKarma.Api.Data.Models.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Mod"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PlatformBanNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.PlatformBan", "Ban") + .WithMany() + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostAddedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostDeletedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostEditedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.Post", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostModDeletedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.PostModAction", "ModAction") + .WithMany() + .HasForeignKey("ModActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModAction"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Notifications.PostModEditedNotification", b => + { + b.HasOne("WowsKarma.Api.Data.Models.PostModAction", "ModAction") + .WithMany() + .HasForeignKey("ModActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModAction"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Clan", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Player", b => + { + b.Navigation("ClanMember"); + + b.Navigation("PlatformBans"); + + b.Navigation("PostsReceived"); + + b.Navigation("PostsSent"); + }); + + modelBuilder.Entity("WowsKarma.Api.Data.Models.Replays.Replay", b => + { + b.Navigation("Post") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WowsKarma.Api/Migrations/ApiDb/20240114120317_UpdateNullableContext.cs b/WowsKarma.Api/Migrations/ApiDb/20240114120317_UpdateNullableContext.cs new file mode 100644 index 00000000..abc09e5b --- /dev/null +++ b/WowsKarma.Api/Migrations/ApiDb/20240114120317_UpdateNullableContext.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Nodsoft.WowsReplaysUnpack.Core.Models; +using WowsKarma.Api.Data.Models.Replays; + +#nullable disable + +namespace WowsKarma.Api.Migrations.ApiDb +{ + /// + public partial class UpdateNullableContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn>( + name: "Players", + table: "Replays", + type: "jsonb", + nullable: false, + oldClrType: typeof(IEnumerable), + oldType: "jsonb", + oldNullable: true); + + migrationBuilder.AlterColumn>( + name: "ChatMessages", + table: "Replays", + type: "jsonb", + nullable: false, + oldClrType: typeof(IEnumerable), + oldType: "jsonb", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "BlobName", + table: "Replays", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ArenaInfo", + table: "Replays", + type: "jsonb", + nullable: false, + oldClrType: typeof(ArenaInfo), + oldType: "jsonb", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "PostModActions", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Username", + table: "Players", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OptOutChanged", + table: "Players", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Tag", + table: "Clans", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Clans", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Description", + table: "Clans", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn>( + name: "Players", + table: "Replays", + type: "jsonb", + nullable: true, + oldClrType: typeof(IEnumerable), + oldType: "jsonb"); + + migrationBuilder.AlterColumn>( + name: "ChatMessages", + table: "Replays", + type: "jsonb", + nullable: true, + oldClrType: typeof(IEnumerable), + oldType: "jsonb"); + + migrationBuilder.AlterColumn( + name: "BlobName", + table: "Replays", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "ArenaInfo", + table: "Replays", + type: "jsonb", + nullable: true, + oldClrType: typeof(ArenaInfo), + oldType: "jsonb"); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "PostModActions", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Username", + table: "Players", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OptOutChanged", + table: "Players", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Tag", + table: "Clans", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Clans", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "Clans", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/WowsKarma.Api/Migrations/ApiDb/ApiDbContextModelSnapshot.cs b/WowsKarma.Api/Migrations/ApiDb/ApiDbContextModelSnapshot.cs index 21a452e0..87968ea9 100644 --- a/WowsKarma.Api/Migrations/ApiDb/ApiDbContextModelSnapshot.cs +++ b/WowsKarma.Api/Migrations/ApiDb/ApiDbContextModelSnapshot.cs @@ -41,6 +41,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValueSql("NOW()"); b.Property("Description") + .IsRequired() .HasColumnType("text"); b.Property("IsDisbanded") @@ -53,9 +54,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("Name") + .IsRequired() .HasColumnType("text"); b.Property("Tag") + .IsRequired() .HasColumnType("text"); b.Property("UpdatedAt") @@ -177,7 +180,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LastBattleTime") .HasColumnType("timestamp with time zone"); - b.Property("OptOutChanged") + b.Property("OptOutChanged") .HasColumnType("timestamp with time zone"); b.Property("OptedOut") @@ -199,6 +202,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("Username") + .IsRequired() .HasColumnType("text"); b.Property("WgAccountCreatedAt") @@ -283,6 +287,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("Reason") + .IsRequired() .HasColumnType("text"); b.HasKey("Id"); @@ -301,18 +306,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("ArenaInfo") + .IsRequired() .HasColumnType("jsonb"); b.Property("BlobName") + .IsRequired() .HasColumnType("text"); b.Property>("ChatMessages") + .IsRequired() .HasColumnType("jsonb"); b.Property("MinimapRendered") .HasColumnType("boolean"); b.Property>("Players") + .IsRequired() .HasColumnType("jsonb"); b.Property("PostId") @@ -578,7 +587,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("WowsKarma.Api.Data.Models.Replays.Replay", b => { - b.Navigation("Post"); + b.Navigation("Post") + .IsRequired(); }); #pragma warning restore 612, 618 } From 55ffe69f0ab5d2043e5ac37180bc8dff1734068a Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sun, 14 Jan 2024 16:12:13 +0100 Subject: [PATCH 15/20] fix(Role): Update Users property to use List instead of IEnumerable The `Role` model's `Users` property has been updated to use a `List` instead of an `IEnumerable`. --- WowsKarma.Api/Data/Models/Auth/Role.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WowsKarma.Api/Data/Models/Auth/Role.cs b/WowsKarma.Api/Data/Models/Auth/Role.cs index a95ba30f..90a63912 100644 --- a/WowsKarma.Api/Data/Models/Auth/Role.cs +++ b/WowsKarma.Api/Data/Models/Auth/Role.cs @@ -14,5 +14,5 @@ public sealed record Role [Required] public string DisplayName { get; set; } = ""; - public IEnumerable Users { get; set; } = []; + public List Users { get; set; } = []; } \ No newline at end of file From 02c09fe5f70629e05fb1343a170c2342f51315d2 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sun, 14 Jan 2024 16:14:51 +0100 Subject: [PATCH 16/20] feat(PlayerController): Refactor SearchAccount API methods - Simplified the implementation of the SearchAccount method in PlayerController. - Replaced conditional statement with a ternary operator for better readability. - Added 'await' keyword to the GetUserAsync method in UserService to ensure asynchronous execution. - Simplified the implementation of the ListPlayersAsync method in PlayerService. - Replaced conditional statement with a ternary operator for better readability. - Updated return type to AccountListingDTO[] instead of IEnumerable. --- WowsKarma.Api/Controllers/PlayerController.cs | 12 ++++-------- WowsKarma.Api/Services/Authentication/UserService.cs | 2 +- WowsKarma.Api/Services/PlayerService.cs | 12 ++++-------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/WowsKarma.Api/Controllers/PlayerController.cs b/WowsKarma.Api/Controllers/PlayerController.cs index 89315662..6102bd4f 100644 --- a/WowsKarma.Api/Controllers/PlayerController.cs +++ b/WowsKarma.Api/Controllers/PlayerController.cs @@ -34,14 +34,10 @@ public PlayerController(PlayerService playerService) /// Account listings for given search query /// No results found for given search query [HttpGet("search/{query}"), ProducesResponseType(typeof(IEnumerable), 200), ProducesResponseType(204)] - public async Task SearchAccount([StringLength(100, MinimumLength = 3), RegularExpression(@"^[a-zA-Z0-9_]*$")] string query) - { - IEnumerable accounts = await _playerService.ListPlayersAsync(query); - - return accounts is null - ? NoContent() - : Ok(accounts); - } + public async Task SearchAccount([StringLength(100, MinimumLength = 3), RegularExpression(@"^[a-zA-Z0-9_]*$")] string query) + => await _playerService.ListPlayersAsync(query) is { Length: not 0 } accounts + ? Ok(accounts) + : NoContent(); /// /// Fetches the player profile for a given Account ID. diff --git a/WowsKarma.Api/Services/Authentication/UserService.cs b/WowsKarma.Api/Services/Authentication/UserService.cs index a9037984..1a3e0233 100644 --- a/WowsKarma.Api/Services/Authentication/UserService.cs +++ b/WowsKarma.Api/Services/Authentication/UserService.cs @@ -31,7 +31,7 @@ public UserService(AuthDbContext context, IHubContext hub /// /// The user's ID. /// The user, or if not found. - public Task GetUserAsync(uint id) => _context.Users.Include(u => u.Roles).FirstOrDefaultAsync(u => u.Id == id); + public async Task GetUserAsync(uint id) => await _context.Users.Include(u => u.Roles).FirstOrDefaultAsync(u => u.Id == id); /// /// Gets a user's claims diff --git a/WowsKarma.Api/Services/PlayerService.cs b/WowsKarma.Api/Services/PlayerService.cs index b1606f8b..ea73160b 100644 --- a/WowsKarma.Api/Services/PlayerService.cs +++ b/WowsKarma.Api/Services/PlayerService.cs @@ -138,14 +138,10 @@ public IEnumerable GetPlayersFullKarma(IEnumerable ac .Where(p => accountIds.Contains(p.Id)) .Select(p => new AccountKarmaDTO(p.Id, p.SiteKarma))); - public async Task> ListPlayersAsync(string search) - { - AccountListing[] result = (await _wgApi.ListPlayersAsync(search))?.Data?.ToArray(); - - return result is { Length: > 0 } - ? result.Select(listing => listing.ToDto()) - : null; - } + public async Task ListPlayersAsync(string search) + => (await _wgApi.ListPlayersAsync(search))?.Data?.ToArray() is { Length: not 0 } result + ? result.Select(Conversions.ToDto).ToArray() + : []; internal async Task UpdatePlayerClanStatusAsync(Player player, CancellationToken ct = default) { From 231448a5da7f147f16ebf5ae74115f368e42b156 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Tue, 23 Jan 2024 08:08:36 +0100 Subject: [PATCH 17/20] feat(PostService): Update PostService to use UTC time for UpdatedAt This commit updates the `PostService` class in the `Posts` service to use UTC time instead of local time when refreshing the `UpdatedAt` property. This change ensures consistency across different time zones and avoids potential issues with daylight saving time changes. --- WowsKarma.Api/Services/Posts/PostService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WowsKarma.Api/Services/Posts/PostService.cs b/WowsKarma.Api/Services/Posts/PostService.cs index d7132d3e..25615e3a 100644 --- a/WowsKarma.Api/Services/Posts/PostService.cs +++ b/WowsKarma.Api/Services/Posts/PostService.cs @@ -165,13 +165,13 @@ public async Task EditPostAsync(Guid id, PlayerPostDTO edited, bool modEditLock ValidatePostContents(edited); Post current = await _context.Posts.FindAsync(id) ?? throw new ArgumentException($"Post {id} not found", nameof(id)); - PostFlairsParsed previousFlairs = current.ParsedFlairs; + PostFlairsParsed? previousFlairs = current.ParsedFlairs; Player player = await _context.Players.FindAsync(current.PlayerId) ?? throw new ArgumentException($"Player Account {edited.Player.Id} not found", nameof(edited)); current.Title = edited.Title; current.Content = edited.Content; current.Flairs = edited.Flairs; - current.UpdatedAt = DateTimeOffset.Now; // Forcing UpdatedAt refresh + current.UpdatedAt = DateTimeOffset.UtcNow; // Forcing UpdatedAt refresh current.ReadOnly = current.ReadOnly || modEditLock; KarmaService.UpdatePlayerKarma(player, current.ParsedFlairs, previousFlairs, current.NegativeKarmaAble); From 1a052031314876ffd70f389d4ca8afbf74a27f2b Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Tue, 23 Jan 2024 08:46:19 +0100 Subject: [PATCH 18/20] build(deps/api): Update dependencies - Updated Nodsoft.WowsReplaysUnpack.ExtendedData to version 2.0.1 - Updated Serilog.AspNetCore to version 8.0.1 --- WowsKarma.Api/WowsKarma.Api.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WowsKarma.Api/WowsKarma.Api.csproj b/WowsKarma.Api/WowsKarma.Api.csproj index 0e37e4e5..ee964521 100644 --- a/WowsKarma.Api/WowsKarma.Api.csproj +++ b/WowsKarma.Api/WowsKarma.Api.csproj @@ -52,9 +52,9 @@ - + - + From b32c393d9e9bac83127e0b8bb5800157db4bbbab Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 25 Jan 2024 12:09:45 +0100 Subject: [PATCH 19/20] fix(api/proffile): Fix bad property validation on profile update The code changes in this commit fix a bug related to property validation on profile updates. Specifically, the `ProfileRoles` property was not being properly validated and could result in unexpected behavior. This issue has been resolved by updating the code in the `ProfileController.cs` and `PlayerService.cs` files. In `ProfileController.cs`, the code has been modified to correctly handle the case where the user's roles are null or empty. Previously, it would return an empty enumerable instead of an empty array. In `PlayerService.cs`, some unnecessary using statements have been removed, which improves code cleanliness and organization. Additionally, in the `UserProfileFlagsDTO.cs` file, the default value for the `ProfileRoles` property has been set to an empty array to ensure consistency and avoid potential issues with null values. --- WowsKarma.Api/Controllers/ProfileController.cs | 2 +- WowsKarma.Api/Services/PlayerService.cs | 1 - WowsKarma.Common/Models/DTOs/UserProfileFlagsDTO.cs | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/WowsKarma.Api/Controllers/ProfileController.cs b/WowsKarma.Api/Controllers/ProfileController.cs index 288a7678..3ce393ab 100644 --- a/WowsKarma.Api/Controllers/ProfileController.cs +++ b/WowsKarma.Api/Controllers/ProfileController.cs @@ -33,7 +33,7 @@ public async Task GetProfileFlagsAsync(uint id) => await _playerS ? Ok(player.Adapt() with { PostsBanned = player.IsBanned(), - ProfileRoles = (await _userService.GetUserAsync(id))?.Roles.Select(r => r.Id) ?? Enumerable.Empty() + ProfileRoles = (await _userService.GetUserAsync(id))?.Roles.Select(r => r.Id) ?? [] }) : NotFound(); diff --git a/WowsKarma.Api/Services/PlayerService.cs b/WowsKarma.Api/Services/PlayerService.cs index ea73160b..3e8dc0a8 100644 --- a/WowsKarma.Api/Services/PlayerService.cs +++ b/WowsKarma.Api/Services/PlayerService.cs @@ -2,7 +2,6 @@ using Hangfire; using Hangfire.Tags.Attributes; using Nodsoft.Wargaming.Api.Client.Clients.Wows; -using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows.Public; using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows.Vortex; using WowsKarma.Api.Data; using WowsKarma.Api.Infrastructure.Exceptions; diff --git a/WowsKarma.Common/Models/DTOs/UserProfileFlagsDTO.cs b/WowsKarma.Common/Models/DTOs/UserProfileFlagsDTO.cs index f854b766..e6db851c 100644 --- a/WowsKarma.Common/Models/DTOs/UserProfileFlagsDTO.cs +++ b/WowsKarma.Common/Models/DTOs/UserProfileFlagsDTO.cs @@ -8,6 +8,6 @@ public sealed record UserProfileFlagsDTO public bool OptedOut { get; init; } public DateTimeOffset OptOutChanged { get; init; } - - public IEnumerable ProfileRoles { get; init; } + + public IEnumerable ProfileRoles { get; init; } = []; } From 1430991c482761aa1c1ee392ffb8c4f8c8b444c1 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Thu, 25 Jan 2024 14:04:25 +0100 Subject: [PATCH 20/20] ci(build): update .NET version to 8.0.x The code changes in this commit update the .NET version used in the build workflow from 7.0.x to 8.0.x. --- .github/workflows/dotnet-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 70a3b13d..86b0948b 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -24,7 +24,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v2 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' # include-prerelease: true - name: Restore dependencies