Stable run using Multi tenancy across entire estate
Audit logging and error logging active
This commit is contained in:
@@ -16,6 +16,30 @@ namespace PostFundManagement.Application.Extensions;
|
||||
|
||||
public static class Services
|
||||
{
|
||||
public static IServiceCollection AddLoggedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface
|
||||
where TCacheDecorator : class, TInterface
|
||||
{
|
||||
services.AddScoped<TImplementation>();
|
||||
services.AddScoped<TCacheDecorator>();
|
||||
|
||||
services.AddScoped(sp =>
|
||||
{
|
||||
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
|
||||
var loggingInterceptor = sp.GetRequiredService<LoggingInterceptor>();
|
||||
|
||||
var actual = sp.GetRequiredService<TImplementation>();
|
||||
|
||||
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
|
||||
|
||||
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
|
||||
loggingInterceptor.ToInterceptor());
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddLoggedAuditedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface
|
||||
@@ -56,11 +80,8 @@ public static class Services
|
||||
services.AddLoggedAuditedCachedServiceScoped<IGrantsService, GrantsService, GrantsCacheService>();
|
||||
services.AddLoggedAuditedCachedServiceScoped<IProjectService, ProjectService, ProjectCacheService>();
|
||||
|
||||
services.AddScoped<IIdentityService, IdentityService>();
|
||||
services.Decorate<IIdentityService, IdentityCacheService>();
|
||||
|
||||
services.AddScoped<IToolService, ToolService>();
|
||||
services.Decorate<IToolService, ToolCacheService>();
|
||||
services.AddLoggedCachedServiceScoped<IIdentityService, IdentityService, IdentityCacheService>();
|
||||
services.AddLoggedCachedServiceScoped<IToolService, ToolService, ToolCacheService>();
|
||||
|
||||
services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
|
||||
@@ -25,8 +25,12 @@ public sealed class ActiveUserFilter(IIdentityService identityService) : IEndpoi
|
||||
var path = context.HttpContext.Request.Path.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path) && !path!.EndsWith("integration/sync", StringComparison.InvariantCultureIgnoreCase))
|
||||
if ((await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted)).IsFailed)
|
||||
{
|
||||
var userAuthResult = await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted);
|
||||
|
||||
if (userAuthResult.IsFailed)
|
||||
return Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
context.HttpContext.Items[UserIdItemKey] = userId;
|
||||
context.HttpContext.Items[EmailtemKey] = email;
|
||||
|
||||
@@ -64,8 +64,8 @@ public sealed class LoggingInterceptor(ILogger<LoggingInterceptor> logger) : IAs
|
||||
|
||||
var errorsString = string.Join("; ", errorMessages);
|
||||
|
||||
logger.LogError("Method {MethodName} failed. Errors: {Errors}. Parameters: {@Parameters}",
|
||||
method.Name, errorsString, parameters);
|
||||
logger.LogError("Method {MethodName} failed. Errors: [{ErrorType}] {Errors}. Parameters: {@Parameters}",
|
||||
method.Name, result.Reasons?.FirstOrDefault()?.GetType().Name.Replace("Error", string.Empty), errorsString, parameters);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class AuditService(IDbContextFactory<ApplicationDbContext> context
|
||||
{
|
||||
EntityId = entityId,
|
||||
Action = action,
|
||||
Changes = new Domain.Models.AuditChanges { Logs = changes },
|
||||
Changes = changes,
|
||||
EntityName = entityName,
|
||||
PerformedBy = performedBy,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ActivityConfiguration, Activity>]
|
||||
public class Activity : Models.Activity, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using PostFundManagement.Domain.Models;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<AuditLogConfiguration, AuditLog>]
|
||||
public sealed class AuditLog : Models.AuditLog;
|
||||
|
||||
public sealed class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
|
||||
@@ -16,9 +16,7 @@ public sealed class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
|
||||
builder.Property(f => f.EntityName).IsRequired().HasMaxLength(LabelLength);
|
||||
builder.Property(f => f.EntityId).IsRequired();
|
||||
builder.Property(f => f.Action).IsRequired().HasMaxLength(ShortLabelLength);
|
||||
builder.Property(f => f.Changes).IsRequired(false).HasColumnType("jsonb")
|
||||
.HasConversion(model => JsonSerializer.Serialize(model, JsonSerializerOptions.Default),
|
||||
provider => JsonSerializer.Deserialize<AuditChanges>(provider, JsonSerializerOptions.Default));
|
||||
builder.Property(f => f.Changes).IsRequired(false);
|
||||
builder.Property(f => f.PerformedBy).IsRequired();
|
||||
builder.Property(f => f.Timestamp).IsRequired().HasDefaultValueSql("now()");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<AwardConfiguration, Award>]
|
||||
public class Award : Models.Award, ITenantEntity
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<BeneficiaryConfiguration, Beneficiary>]
|
||||
public class Beneficiary : Models.Beneficiary, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<BudgetConfiguration, Budget>]
|
||||
public class Budget : Models.Budget, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ChangeRequestConfiguration, ChangeRequest>]
|
||||
public class ChangeRequest : Models.ChangeRequest, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ComplianceItemConfiguration, ComplianceItem>]
|
||||
public class ComplianceItem : Models.ComplianceItem, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ContractConfiguration, Contract>]
|
||||
public class Contract : Models.Contract, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<DisbursementConfiguration, Disbursement>]
|
||||
public class Disbursement : Models.Disbursement, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<EvidenceConfiguration, Evidence>]
|
||||
public class Evidence : Models.Evidence, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<IndicatorConfiguration, Indicator>]
|
||||
public class Indicator : Models.Indicator, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<InstrumentConfiguration, Instrument>]
|
||||
public class Instrument : Models.Instrument, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<InviteConfiguration, Invite>]
|
||||
public class Invite : Models.Invite, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<IssueConfiguration, Issue>]
|
||||
public class Issue : Models.Issue, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<MilestoneConfiguration, Milestone>]
|
||||
public class Milestone : Models.Milestone, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<NotificationConfiguration, Notification>]
|
||||
public class Notification : Models.Notification, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -2,6 +2,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<OrganisationConfiguration, Organisation>]
|
||||
public class Organisation : Models.Organisation
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
public class Outcome : Models.Outcome
|
||||
[EntityTypeConfiguration<OutcomeConfiguration, Outcome>]
|
||||
public class Outcome : Models.Outcome, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
public virtual Beneficiary? Beneficiary { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
@@ -16,12 +20,19 @@ public sealed class OutcomeConfiguration : IEntityTypeConfiguration<Outcome>
|
||||
builder.ToTable(nameof(Outcome).Pluralize());
|
||||
|
||||
builder.HasKey(pk => pk.Id);
|
||||
builder.Property(f => f.TenantId).IsRequired();
|
||||
builder.Property(f => f.BeneficiaryId).IsRequired();
|
||||
builder.Property(f => f.CreatedBy).IsRequired();
|
||||
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
|
||||
builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength);
|
||||
|
||||
builder.HasOne(f => f.Tenant)
|
||||
.WithMany()
|
||||
.HasForeignKey(fk => fk.TenantId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
|
||||
builder.HasOne(f => f.Beneficiary)
|
||||
.WithMany()
|
||||
.HasForeignKey(fk => fk.BeneficiaryId)
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<PortfolioConfiguration, Portfolio>]
|
||||
public class Portfolio : Models.Portfolio, ITenantEntity
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ProgrammeConfiguration, Programme>]
|
||||
public class Programme : Models.Programme, ITenantEntity
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ProjectConfiguration, Project>]
|
||||
public class Project : Models.Project, ITenantEntity
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<RiskConfiguration, Risk>]
|
||||
public class Risk : Models.Risk, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<RuleConfiguration, Rule>]
|
||||
public class Rule : Models.Rule, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<SiteVisitConfiguration, SiteVisit>]
|
||||
public class SiteVisit : Models.SiteVisit, ITenantEntity
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<TenantConfiguration, Tenant>]
|
||||
public class Tenant : Models.Tenant
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<TenantOrganisationConfiguration, TenantOrganisation>]
|
||||
public class TenantOrganisation : Models.TenantOrganisation
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
@@ -16,8 +17,6 @@ public sealed class TenantOrganisationConfiguration : IEntityTypeConfiguration<T
|
||||
builder.ToTable(nameof(TenantOrganisation).Pluralize());
|
||||
|
||||
builder.HasKey(pk => pk.Id);
|
||||
builder.HasIndex(to => new { to.TenantId, to.OrganisationId }).IsUnique();
|
||||
|
||||
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.CreatedBy).IsRequired();
|
||||
builder.Property(f => f.TenantId).IsRequired();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<TenantUserConfiguration, TenantUser>]
|
||||
public class TenantUser : Models.TenantUser
|
||||
{
|
||||
public virtual Tenant? Tenant { get; set; }
|
||||
@@ -17,9 +18,7 @@ public sealed class TenantUserConfiguration : IEntityTypeConfiguration<TenantUse
|
||||
{
|
||||
builder.ToTable(nameof(TenantUser).Pluralize());
|
||||
|
||||
builder.HasKey(pk => pk.Id);
|
||||
builder.HasIndex(tu => new { tu.TenantId, tu.UserId }).IsUnique();
|
||||
|
||||
builder.HasKey(pk => pk.Id);
|
||||
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.CreatedBy).IsRequired();
|
||||
builder.Property(f => f.UserId).IsRequired();
|
||||
|
||||
@@ -2,6 +2,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<UserConfiguration, User>]
|
||||
public sealed class User : Models.User;
|
||||
|
||||
public sealed class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
|
||||
@@ -12,8 +12,7 @@ public class AuditLog
|
||||
|
||||
public string? Action { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public AuditChanges? Changes { get; set; }
|
||||
public string[]? Changes { get; set; }
|
||||
|
||||
public Guid PerformedBy { get; set; }
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ public class Outcome
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long TenantId { get; set; }
|
||||
|
||||
public long BeneficiaryId { get; set; }
|
||||
|
||||
public Guid CreatedBy { get; set; }
|
||||
|
||||
@@ -73,8 +73,6 @@ public sealed class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType))
|
||||
@@ -89,8 +87,11 @@ public sealed class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
|
||||
modelBuilder.Entity<Organisation>().HasQueryFilter(o => BypassFilter ||
|
||||
Set<TenantOrganisation>().Any(to => to.TenantId == CurrentTenantId && to.OrganisationId == o.Id));
|
||||
|
||||
modelBuilder.Entity<User>().HasQueryFilter(o => BypassFilter ||
|
||||
Set<TenantUser>().Any(to => to.TenantId == CurrentTenantId && to.UserId == o.Id));
|
||||
modelBuilder.Entity<TenantOrganisation>()
|
||||
.HasQueryFilter(to => BypassFilter || to.TenantId == CurrentTenantId);
|
||||
|
||||
modelBuilder.Entity<TenantUser>()
|
||||
.HasQueryFilter(to => BypassFilter || to.TenantId == CurrentTenantId);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
|
||||
+2285
File diff suppressed because it is too large
Load Diff
+50
@@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PostFundManagement.Infrastructure.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedTenantIdToOutcomes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "TenantId",
|
||||
table: "Outcomes",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Outcomes_TenantId",
|
||||
table: "Outcomes",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Outcomes_Tenants_TenantId",
|
||||
table: "Outcomes",
|
||||
column: "TenantId",
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Outcomes_Tenants_TenantId",
|
||||
table: "Outcomes");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Outcomes_TenantId",
|
||||
table: "Outcomes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TenantId",
|
||||
table: "Outcomes");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2445
File diff suppressed because it is too large
Load Diff
+5599
File diff suppressed because it is too large
Load Diff
+542
-369
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user