This commit is contained in:
Tomasi - Developing 2025-02-06 10:44:55 +01:00
parent 3c19151def
commit bafe2cde5a
50 changed files with 2928 additions and 165 deletions

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@ -20,6 +20,7 @@
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="7.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,16 +1,17 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Api.Helpers;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.OpenApi.Models;
namespace Api.Configurations;
public static class SwaggerExtension
{
// Configures Swagger for API documentation
public static void ConfigureAndAddSwagger(this IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
// Defines API documentation details for version 1
options.EnableAnnotations();
options.DocumentFilter<HideEndpointsInProductionFilter>();
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Last War Player Management API",

View File

@ -0,0 +1,100 @@
using Application.DataTransferObjects.ApiKey;
using Application.Errors;
using Application.Interfaces;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Api.Controllers.v1
{
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[Authorize]
public class ApiKeysController(IApiKeyRepository apiKeyRepository, ILogger<ApiKeysController> logger, IClaimTypeService claimTypeService) : ControllerBase
{
[HttpGet("{allianceId:guid}")]
public async Task<ActionResult<ApiKeyDto>> GetAllianceApiKey(Guid allianceId,
CancellationToken cancellationToken)
{
try
{
var allianceApiKeyResult =
await apiKeyRepository.GetApiKeyByAllianceIdAsync(allianceId, cancellationToken);
return allianceApiKeyResult.IsFailure
? BadRequest(allianceApiKeyResult.Error)
: Ok(allianceApiKeyResult.Value);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost]
public async Task<ActionResult<ApiKeyDto>> CreateApiKey(CreateApiKeyDto createApiKeyDto,
CancellationToken cancellationToken)
{
try
{
if (!ModelState.IsValid) return UnprocessableEntity(ModelState);
var createResult = await apiKeyRepository.CreateApiKeyAsync(createApiKeyDto,
claimTypeService.GetFullName(User), cancellationToken);
return createResult.IsFailure
? BadRequest(createResult.Error)
: Ok(createResult.Value);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPut("{apiKeyId:guid}")]
public async Task<ActionResult<ApiKeyDto>> UpdateApiKey(Guid apiKeyId, UpdateApiKeyDto updateApiKeyDto,
CancellationToken cancellationToken)
{
try
{
if (!ModelState.IsValid) return UnprocessableEntity(ModelState);
if (updateApiKeyDto.Id != apiKeyId) return Conflict(ApiKeyErrors.IdConflict);
var updateResult = await apiKeyRepository.UpdateApiKeyAsync(updateApiKeyDto,
claimTypeService.GetFullName(User), cancellationToken);
return updateResult.IsFailure
? BadRequest(updateResult.Error)
: Ok(updateResult.Value);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpDelete("{apiKeyId:guid}")]
public async Task<ActionResult<bool>> DeleteApiKey(Guid apiKeyId, CancellationToken cancellationToken)
{
try
{
var deleteResult = await apiKeyRepository.DeleteApiKeyAsync(apiKeyId, cancellationToken);
return deleteResult.IsFailure
? BadRequest(deleteResult.Error)
: Ok(deleteResult.Value);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
}

View File

@ -1,4 +1,5 @@
using Application.DataTransferObjects;
using Api.Helpers;
using Application.DataTransferObjects;
using Application.DataTransferObjects.ExcelImport;
using Application.DataTransferObjects.Player;
using Application.Errors;
@ -6,6 +7,7 @@ using Application.Interfaces;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers.v1
@ -13,9 +15,9 @@ namespace Api.Controllers.v1
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[Authorize]
public class PlayersController(IPlayerRepository playerRepository, IClaimTypeService claimTypeService, IExcelService excelService, ILogger<PlayersController> logger) : ControllerBase
{
[Authorize]
[HttpGet("{playerId:guid}")]
public async Task<ActionResult<PlayerDto>> GetPlayer(Guid playerId, CancellationToken cancellationToken)
{
@ -34,6 +36,7 @@ namespace Api.Controllers.v1
}
}
[Authorize]
[HttpGet("Alliance/{allianceId:guid}")]
public async Task<ActionResult<List<PlayerDto>>> GetAlliancePlayers(Guid allianceId, CancellationToken cancellationToken)
{
@ -55,6 +58,7 @@ namespace Api.Controllers.v1
}
}
[Authorize]
[HttpGet("Alliance/dismiss/{allianceId:guid}")]
public async Task<ActionResult<PagedResponseDto<PlayerDto>>> GetAllianceDismissPlayers(Guid allianceId, CancellationToken cancellationToken, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
{
@ -76,6 +80,7 @@ namespace Api.Controllers.v1
}
}
[Authorize]
[HttpGet("DismissInformation/{playerId:guid}")]
public async Task<ActionResult<DismissPlayerInformationDto>> GetDismissPlayerInformation(Guid playerId, CancellationToken cancellationToken)
{
@ -141,6 +146,32 @@ namespace Api.Controllers.v1
}
}
[AllowApiKey]
[HttpGet("Mvp/")]
[SwaggerOperation(
Summary = "Get Alliance MVPs",
Description = "Retrieves the MVPs (Most Valuable Players) for a given alliance. If the 'playerType' parameter is not provided, all MVPs will be returned. The 'playerType' parameter specifies whether the MVPs should be from 'players' or 'leadership'. Possible values for 'playerType' are 'players' or 'leadership'."
)]
public async Task<ActionResult<List<PlayerMvpDto>>> GetAllianceMvp([FromQuery] Guid allianceId, [FromQuery]string? playerType, [FromQuery]string? key, CancellationToken cancellationToken)
{
try
{
var allianceMvpResult = await playerRepository.GetAllianceMvp(allianceId, playerType, cancellationToken);
if (allianceMvpResult.IsFailure) return BadRequest(allianceMvpResult.Error);
return allianceMvpResult.Value.Count > 0
? Ok(allianceMvpResult.Value)
: NoContent();
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[Authorize]
[HttpPost]
public async Task<ActionResult<PlayerDto>> CreatePlayer(CreatePlayerDto createPlayerDto, CancellationToken cancellationToken)
{
@ -161,6 +192,7 @@ namespace Api.Controllers.v1
}
}
[Authorize]
[HttpPost("ExcelImport")]
public async Task<ActionResult<ExcelImportResponse>> ImportPlayersFromExcel(
[FromForm] ExcelImportRequest excelImportRequest, CancellationToken cancellationToken)
@ -194,6 +226,7 @@ namespace Api.Controllers.v1
}
}
[Authorize]
[HttpPut("{playerId:guid}")]
public async Task<ActionResult<PlayerDto>> UpdatePlayer(Guid playerId, UpdatePlayerDto updatePlayerDto, CancellationToken cancellationToken)
{
@ -216,6 +249,7 @@ namespace Api.Controllers.v1
}
}
[Authorize]
[HttpPut("{playerId:guid}/dismiss")]
public async Task<ActionResult<PlayerDto>> DismissPlayer(Guid playerId, DismissPlayerDto dismissPlayerDto,
CancellationToken cancellationToken)
@ -239,6 +273,7 @@ namespace Api.Controllers.v1
}
}
[Authorize]
[HttpPut("{playerId:guid}/reactive")]
public async Task<ActionResult<PlayerDto>> ReactivePlayer(Guid playerId, ReactivatePlayerDto reactivatePlayerDto, CancellationToken cancellationToken)
{
@ -261,6 +296,7 @@ namespace Api.Controllers.v1
}
}
[Authorize]
[HttpDelete("{playerId:guid}")]
public async Task<ActionResult<bool>> DeletePlayer(Guid playerId, CancellationToken cancellationToken)
{

View File

@ -0,0 +1,26 @@
using Api.Helpers;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.WebEncoders.Testing;
namespace Api.Controllers.v1
{
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
public class ValuesController : ControllerBase
{
[AllowApiKey]
[HttpGet]
public async Task<IActionResult> Test([FromQuery] Guid allianceId, [FromQuery] string? key)
{
return Ok(new
{
AllianceId = allianceId,
Key = key
});
}
}
}

View File

@ -0,0 +1,7 @@
namespace Api.Helpers;
[AttributeUsage(AttributeTargets.Method)]
public class AllowApiKeyAttribute : Attribute
{
}

View File

@ -0,0 +1,41 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Api.Helpers;
public class HideEndpointsInProductionFilter(IWebHostEnvironment environment) : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (environment.IsProduction())
{
var allowedPaths = new List<string>
{
"/api/v{version}/Players/Mvp"
};
var allowedSchemas = new List<string>
{
"PlayerMvpDto"
};
var pathsToRemove = swaggerDoc.Paths
.Where(path => !allowedPaths.Any(allowedPath => path.Key.Contains(allowedPath)))
.ToList();
foreach (var path in pathsToRemove)
{
swaggerDoc.Paths.Remove(path.Key);
}
var schemaToRemove = context.SchemaRepository.Schemas
.Where(schema => !allowedSchemas.Contains(schema.Key))
.ToList();
foreach (var schema in schemaToRemove)
{
context.SchemaRepository.Schemas.Remove(schema.Key);
}
}
}
}

View File

@ -0,0 +1,120 @@
using System.Text.Json;
using Api.Helpers;
using Application.Interfaces;
using Database.Entities;
using Microsoft.AspNetCore.Mvc;
namespace Api.Middleware;
public class ApiKeyMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory, IEncryptionService encryptionService)
{
private const string ApiKeyHeaderName = "X-Api-Key";
private const string AllianzIdQueryParamName = "AllianceId";
private const string ApiKeyQueryParamName = "Key";
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated == true)
{
await next(context);
return;
}
var endpoint = context.GetEndpoint();
var allowApiKeyAttribute = endpoint?.Metadata.GetMetadata<AllowApiKeyAttribute>();
if (allowApiKeyAttribute is null)
{
await next(context);
return;
}
if (!context.Request.Headers.ContainsKey(ApiKeyHeaderName) && !context.Request.Query.ContainsKey(ApiKeyQueryParamName))
{
await WriteProblemDetailsResponse(context, StatusCodes.Status401Unauthorized, "Unauthorized", "API Key is required.");
return;
}
if (!context.Request.Query.ContainsKey(AllianzIdQueryParamName))
{
await WriteProblemDetailsResponse(context, StatusCodes.Status400BadRequest, "Bad Request", "AllianceId is required.");
return;
}
if (!Guid.TryParse(context.Request.Query[AllianzIdQueryParamName], out var allianceId))
{
await WriteProblemDetailsResponse(context, StatusCodes.Status400BadRequest, "Bad Request", "The provided AllianceId is not a valid GUID.");
return;
}
var apiKey = context.Request.Headers.ContainsKey(ApiKeyHeaderName)
? context.Request.Headers[ApiKeyHeaderName]
: context.Request.Query[ApiKeyQueryParamName];
using var scope = scopeFactory.CreateScope();
var apiKeyRepository = scope.ServiceProvider.GetRequiredService<IApiKeyRepository>();
var apiKeyResult = await apiKeyRepository.GetAllianceApiKeyAsync(allianceId);
if (apiKeyResult.IsFailure)
{
await WriteProblemDetailsResponse(context, StatusCodes.Status401Unauthorized, "Unauthorized", "The provided API Key is not valid.");
return;
}
if (!await IsValidApiKeyAsync(apiKeyResult.Value, apiKey!))
{
await WriteProblemDetailsResponse(context, StatusCodes.Status401Unauthorized, "Unauthorized", "The API Key does not match.");
return;
}
await next(context);
}
private async Task<bool> IsValidApiKeyAsync(ApiKey apiKey, string key)
{
var decryptedKey = await encryptionService.Decrypt(apiKey.EncryptedKey).ConfigureAwait(false);
return key == decryptedKey;
}
private static async Task WriteProblemDetailsResponse(HttpContext context, int statusCode, string title,
string detail)
{
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/problem+json";
var type = GetErrorTypeForStatusCode(statusCode);
var problemDetail = new ProblemDetails
{
Type = type,
Status = statusCode,
Title = title,
Detail = detail,
Instance = context.Request.Path,
Extensions =
{
["traceId"] = context.TraceIdentifier,
["timestamp"] = DateTime.UtcNow.ToString("o"),
["method"] = context.Request.Method
}
};
await context.Response.WriteAsJsonAsync(problemDetail,
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
}
private static string GetErrorTypeForStatusCode(int statusCode)
{
return statusCode switch
{
400 => "https://tools.ietf.org/html/rfc9110#section-15.5.1",
401 => "https://tools.ietf.org/html/rfc9110#section-15.2.2",
404 => "https://tools.ietf.org/html/rfc9110#section-15.5.5",
500 => "https://tools.ietf.org/html/rfc9110#section-15.6.1",
_ => "https://tools.ietf.org/html/rfc9110#section-15.6.5"
};
}
}

View File

@ -1,6 +1,7 @@
// Configure Serilog logger
using Api.Configurations;
using Api.Middleware;
using Application;
using Database;
using HealthChecks.UI.Client;
@ -60,6 +61,7 @@ try
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMiddleware<ApiKeyMiddleware>();
app.UseAuthorization();
app.MapControllers();

View File

@ -20,6 +20,7 @@
<PackageReference Include="ExcelDataReader" Version="3.7.0" />
<PackageReference Include="ExcelDataReader.DataSet" Version="3.7.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="7.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,4 +1,5 @@
using System.Reflection;
using Application.Helpers;
using Application.Interfaces;
using Application.Repositories;
using Application.Services;
@ -32,12 +33,14 @@ public static class ApplicationDependencyInjection
services.AddScoped<IZombieSiegeRepository, ZombieSiegeRepository>();
services.AddScoped<IZombieSiegeParticipantRepository, ZombieSiegeParticipantRepository>();
services.AddScoped<IVsDuelLeagueRepository, VsDuelLeagueRepository>();
services.AddScoped<IExcelService, ExcelService>();
services.AddScoped<IApiKeyRepository, ApiKeyRepository>();
services.AddTransient<IJwtService, JwtService>();
services.AddTransient<IClaimTypeService, ClaimTypeService>();
services.AddTransient<IEmailService, EmailService>();
services.AddTransient<IExcelService, ExcelService>();
services.AddTransient<IEncryptionService, EncryptionService>();
return services;
}

View File

@ -0,0 +1,17 @@
namespace Application.DataTransferObjects.ApiKey;
public class ApiKeyDto
{
public Guid Id { get; set; }
public Guid AllianceId { get; set; }
public required string Key { get; set; }
public DateTime CreatedOn { get; set; }
public required string CreatedBy { get; set; }
public DateTime? ModifiedOn { get; set; }
public string? ModifiedBy { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace Application.DataTransferObjects.ApiKey;
public class CreateApiKeyDto
{
public Guid AllianceId { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace Application.DataTransferObjects.ApiKey;
public class UpdateApiKeyDto
{
public Guid Id { get; set; }
public Guid AllianceId { get; set; }
}

View File

@ -1,12 +1,27 @@
namespace Application.DataTransferObjects.Player;
using Swashbuckle.AspNetCore.Annotations;
namespace Application.DataTransferObjects.Player;
public class PlayerMvpDto
{
public required string PlayerName { get; set; }
public required string Rank { get; set; }
public long TotalVsDuelPoints { get; set; }
public int MarshalGuardParticipationCount { get; set; }
[SwaggerSchema(Description = "Name of the player.")]
public required string Name { get; set; }
[SwaggerSchema(Description = "The alliance rank of the player, which can be R5, R4, R3, R2, or R1.")]
public required string AllianceRank { get; set; }
[SwaggerSchema(Description = "Total points the player earned in the last three weeks based on duel performance.")]
public long DuelPointsLast3Weeks { get; set; }
[SwaggerSchema(Description = "The number of times the player participated in the Marshal competition in the last three weeks.")]
public int MarshalParticipationCount { get; set; }
[SwaggerSchema(Description = "The number of times the player participated in the Desert Storm competition in the last three weeks.")]
public int DesertStormParticipationCount { get; set; }
public bool IsOldestVsDuelParticipated { get; set; }
public decimal MvpPoints { get; set; }
[SwaggerSchema(Description = "Indicates whether the player has participated in the oldest VS Duel for at least the last three weeks.")]
public bool HasParticipatedInOldestDuel { get; set; }
[SwaggerSchema(Description = "The total points earned by the player according to the MVP formula.")]
public decimal MvpScore { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace Application.Errors;
public static class ApiKeyErrors
{
public static readonly Error NotFound = new("Error.ApiKey.NotFound",
"The api key with the specified identifier was not found");
public static readonly Error NoKeyForAlliance = new("Error.ApiKey.NoKeyForAlliance",
"The alliance has no api key");
public static readonly Error IdConflict = new("Error.ApiKey.IdConflict", "There is a conflict with the id's");
}

View File

@ -0,0 +1,53 @@
using System.Security.Cryptography;
using System.Text;
using Application.Interfaces;
using Microsoft.Extensions.Configuration;
using Aes = System.Security.Cryptography.Aes;
namespace Application.Helpers;
public class EncryptionService : IEncryptionService
{
private readonly byte[] _key;
private readonly byte[] _iv;
public EncryptionService(IConfiguration configuration)
{
var encryptionSection = configuration.GetRequiredSection("Encryption");
_key = Encoding.UTF8.GetBytes(encryptionSection["Key"] ?? throw new InvalidOperationException());
_iv = Encoding.UTF8.GetBytes(encryptionSection["Iv"] ?? throw new InvalidOperationException());
}
public async Task<string> EncryptAsync(string plainText)
{
if (string.IsNullOrEmpty(plainText)) throw new ArgumentNullException(nameof(plainText));
using var aes = Aes.Create();
aes.Key = _key;
aes.IV = _iv;
using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using var memoryStream = new MemoryStream();
await using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
var inputBytes = Encoding.UTF8.GetBytes(plainText);
await cryptoStream.WriteAsync(inputBytes, 0, inputBytes.Length);
await cryptoStream.FlushFinalBlockAsync();
return Convert.ToBase64String(memoryStream.ToArray());
}
public async Task<string> Decrypt(string encryptedText)
{
if (string.IsNullOrEmpty(encryptedText)) throw new ArgumentNullException(nameof(encryptedText));
using var aes = Aes.Create();
aes.Key = _key;
aes.IV = _iv;
using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using var memoryStream = new MemoryStream(Convert.FromBase64String(encryptedText));
await using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
using var reader = new StreamReader(cryptoStream, Encoding.UTF8);
return await reader.ReadToEndAsync();
}
}

View File

@ -0,0 +1,18 @@
using Application.Classes;
using Application.DataTransferObjects.ApiKey;
using Database.Entities;
namespace Application.Interfaces;
public interface IApiKeyRepository
{
Task<Result<ApiKey>> GetAllianceApiKeyAsync(Guid allianceId);
Task<Result<ApiKeyDto>> GetApiKeyByAllianceIdAsync(Guid allianceId, CancellationToken cancellationToken);
Task<Result<ApiKeyDto>> CreateApiKeyAsync(CreateApiKeyDto createApiKeyDto, string creator, CancellationToken cancellationToken);
Task<Result<ApiKeyDto>> UpdateApiKeyAsync(UpdateApiKeyDto updateApiKeyDto, string modifier, CancellationToken cancellationToken);
Task<Result<bool>> DeleteApiKeyAsync(Guid apiKeyId, CancellationToken cancellationToken);
}

View File

@ -0,0 +1,8 @@
namespace Application.Interfaces;
public interface IEncryptionService
{
Task<string> EncryptAsync(string plainText);
Task<string> Decrypt(string encryptedText);
}

View File

@ -16,6 +16,8 @@ public interface IPlayerRepository
Task<Result<List<PlayerMvpDto>>> GetAllianceLeadershipMvp(Guid allianceId, CancellationToken cancellationToken);
Task<Result<List<PlayerMvpDto>>> GetAllianceMvp(Guid allianceId, string? playerType, CancellationToken cancellationToken);
Task<Result<DismissPlayerInformationDto>> GetDismissPlayerInformationAsync(Guid playerId, CancellationToken cancellationToken);
Task<Result<PlayerDto>> CreatePlayerAsync(CreatePlayerDto createPlayerDto, string createdBy, CancellationToken cancellationToken);

View File

@ -15,7 +15,6 @@ public class PlayerProfile : Profile
CreateMap<CreatePlayerDto, Player>()
.ForMember(des => des.Id, opt => opt.MapFrom(src => Guid.CreateVersion7()))
.ForMember(des => des.IsDismissed, opt => opt.MapFrom(src => false))
.ForMember(des => des.CreatedOn, opt => opt.MapFrom(src => DateTime.Now));
CreateMap<UpdatePlayerDto, Player>()

View File

@ -0,0 +1,135 @@
using Application.Classes;
using Application.DataTransferObjects.ApiKey;
using Application.Errors;
using Application.Interfaces;
using Database;
using Database.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Application.Repositories;
public class ApiKeyRepository(ApplicationContext context, ILogger<ApiKeyRepository> logger, IEncryptionService encryptionService) : IApiKeyRepository
{
public async Task<Result<ApiKey>> GetAllianceApiKeyAsync(Guid allianceId)
{
var allianceApiKey = await context.ApiKeys
.AsNoTracking()
.FirstOrDefaultAsync(apiKey => apiKey.AllianceId == allianceId);
return allianceApiKey is null
? Result.Failure<ApiKey>(ApiKeyErrors.NoKeyForAlliance)
: Result.Success(allianceApiKey);
}
public async Task<Result<ApiKeyDto>> GetApiKeyByAllianceIdAsync(Guid allianceId, CancellationToken cancellationToken)
{
var allianceApiKey = await context.ApiKeys
.AsNoTracking()
.FirstOrDefaultAsync(apiKey => apiKey.AllianceId == allianceId, cancellationToken);
if (allianceApiKey is null) return Result.Failure<ApiKeyDto>(ApiKeyErrors.NoKeyForAlliance);
return Result.Success(new ApiKeyDto()
{
Id = allianceApiKey.Id,
CreatedBy = allianceApiKey.CreatedBy,
CreatedOn = allianceApiKey.CreatedOn,
AllianceId = allianceApiKey.AllianceId,
ModifiedBy = allianceApiKey.ModifiedBy,
ModifiedOn = allianceApiKey.ModifiedOn,
Key = await encryptionService.Decrypt(allianceApiKey.EncryptedKey)
});
}
public async Task<Result<ApiKeyDto>> CreateApiKeyAsync(CreateApiKeyDto createApiKeyDto, string creator, CancellationToken cancellationToken)
{
try
{
var apiKey = Guid.NewGuid().ToString("N");
var newApiKey = new ApiKey()
{
Id = Guid.CreateVersion7(),
CreatedBy = creator,
EncryptedKey = await encryptionService.EncryptAsync(apiKey),
AllianceId = createApiKeyDto.AllianceId,
CreatedOn = DateTime.Now
};
await context.ApiKeys.AddAsync(newApiKey, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
return Result.Success(new ApiKeyDto()
{
Id = newApiKey.Id,
CreatedBy = newApiKey.CreatedBy,
CreatedOn = newApiKey.CreatedOn,
AllianceId = newApiKey.AllianceId,
ModifiedBy = newApiKey.ModifiedBy,
ModifiedOn = newApiKey.ModifiedOn,
Key = apiKey
});
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return Result.Failure<ApiKeyDto>(GeneralErrors.DatabaseError);
}
}
public async Task<Result<ApiKeyDto>> UpdateApiKeyAsync(UpdateApiKeyDto updateApiKeyDto, string modifier, CancellationToken cancellationToken)
{
try
{
var apiKeyToUpdate = await context.ApiKeys
.FirstOrDefaultAsync(apiKey => apiKey.Id == updateApiKeyDto.Id, cancellationToken);
if (apiKeyToUpdate == null) return Result.Failure<ApiKeyDto>(ApiKeyErrors.NotFound);
var newApiKey = Guid.NewGuid().ToString("N");
apiKeyToUpdate.EncryptedKey = await encryptionService.EncryptAsync(newApiKey);
apiKeyToUpdate.ModifiedBy = modifier;
apiKeyToUpdate.ModifiedOn = DateTime.Now;
await context.SaveChangesAsync(cancellationToken);
return Result.Success(new ApiKeyDto()
{
Id = apiKeyToUpdate.Id,
CreatedBy = apiKeyToUpdate.CreatedBy,
CreatedOn = apiKeyToUpdate.CreatedOn,
AllianceId = apiKeyToUpdate.AllianceId,
ModifiedBy = apiKeyToUpdate.ModifiedBy,
ModifiedOn = apiKeyToUpdate.ModifiedOn,
Key = newApiKey
});
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return Result.Failure<ApiKeyDto>(GeneralErrors.DatabaseError);
}
}
public async Task<Result<bool>> DeleteApiKeyAsync(Guid apiKeyId, CancellationToken cancellationToken)
{
try
{
var apiKeyToDelete = await context.ApiKeys
.FirstOrDefaultAsync(apiKey => apiKey.Id == apiKeyId, cancellationToken);
if (apiKeyToDelete == null) return Result.Failure<bool>(ApiKeyErrors.NotFound);
context.ApiKeys.Remove(apiKeyToDelete);
await context.SaveChangesAsync(cancellationToken);
return Result.Success(true);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return Result.Failure<bool>(GeneralErrors.DatabaseError);
}
}
}

View File

@ -63,6 +63,67 @@ public class PlayerRepository(ApplicationContext context, IMapper mapper, ILogge
});
}
public async Task<Result<List<PlayerMvpDto>>> GetAllianceMvp(Guid allianceId, string? playerType, CancellationToken cancellationToken)
{
var currentDate = DateTime.Now;
var threeWeeksAgo = currentDate.AddDays(-21);
var query = context.Players.Where(p => p.AllianceId == allianceId);
query = playerType switch
{
"players" => query.Where(p => p.Rank.Name != "R4" && p.Rank.Name != "R5"),
"leadership" => query.Where(p => p.Rank.Name == "R4" || p.Rank.Name == "R5"),
_ => query
};
var playerMvps = await query
.Select(p => new
{
p.Id,
p.PlayerName,
Rank = p.Rank.Name,
VsDuels = context.VsDuelParticipants
.Where(vp => vp.PlayerId == p.Id && vp.VsDuel.EventDate <= currentDate && !vp.VsDuel.IsInProgress)
.OrderByDescending(vp => vp.VsDuel.EventDate)
.Take(3)
.Sum(vp => vp.WeeklyPoints),
IsOldestVsDuelParticipated = context.VsDuelParticipants
.Where(vp => vp.PlayerId == p.Id && vp.VsDuel.EventDate <= currentDate && !vp.VsDuel.IsInProgress)
.OrderByDescending(vp => vp.VsDuel.EventDate)
.Skip(2)
.Take(1)
.Any(),
MarshalGuardParticipationCount = context.MarshalGuardParticipants
.Count(mpg => mpg.PlayerId == p.Id && mpg.Participated && mpg.MarshalGuard.EventDate > threeWeeksAgo),
DessertStormParticipationCount = context.DesertStormParticipants
.Count(dsp => dsp.PlayerId == p.Id && dsp.Participated && dsp.DesertStorm.EventDate > threeWeeksAgo)
})
.Select(p => new PlayerMvpDto()
{
Name = p.PlayerName,
AllianceRank = p.Rank,
DuelPointsLast3Weeks = p.VsDuels,
MarshalParticipationCount = p.MarshalGuardParticipationCount,
DesertStormParticipationCount = p.DessertStormParticipationCount,
HasParticipatedInOldestDuel = p.IsOldestVsDuelParticipated,
MvpScore = Math.Round(
(decimal)((p.VsDuels / 1000000.0 * 0.8) +
((p.MarshalGuardParticipationCount * 20 + p.DessertStormParticipationCount * 40) * 0.2)), 2)
})
.OrderByDescending(p => p.MvpScore)
.ThenByDescending(p => p.DuelPointsLast3Weeks)
.ThenByDescending(p => p.MarshalParticipationCount)
.ThenBy(p => p.Name)
.ToListAsync(cancellationToken);
return playerMvps;
}
public async Task<Result<List<PlayerMvpDto>>> GetAlliancePlayersMvp(Guid allianceId, CancellationToken cancellationToken)
{
var currentDate = DateTime.Now;
@ -97,20 +158,20 @@ public class PlayerRepository(ApplicationContext context, IMapper mapper, ILogge
})
.Select(p => new PlayerMvpDto()
{
PlayerName = p.PlayerName,
Rank = p.Rank,
TotalVsDuelPoints = p.VsDuels,
MarshalGuardParticipationCount = p.MarshalGuardParticipationCount,
Name = p.PlayerName,
AllianceRank = p.Rank,
DuelPointsLast3Weeks = p.VsDuels,
MarshalParticipationCount = p.MarshalGuardParticipationCount,
DesertStormParticipationCount = p.DessertStormParticipationCount,
IsOldestVsDuelParticipated = p.IsOldestVsDuelParticipated,
MvpPoints = Math.Round(
HasParticipatedInOldestDuel = p.IsOldestVsDuelParticipated,
MvpScore = Math.Round(
(decimal)((p.VsDuels / 1000000.0 * 0.8) +
((p.MarshalGuardParticipationCount * 20 + p.DessertStormParticipationCount * 40) * 0.2)),2)
})
.OrderByDescending(p => p.MvpPoints)
.ThenByDescending(p => p.TotalVsDuelPoints)
.ThenByDescending(p => p.MarshalGuardParticipationCount)
.ThenBy(p => p.PlayerName)
.OrderByDescending(p => p.MvpScore)
.ThenByDescending(p => p.DuelPointsLast3Weeks)
.ThenByDescending(p => p.MarshalParticipationCount)
.ThenBy(p => p.Name)
.ToListAsync(cancellationToken);
return playerMvps;
@ -143,19 +204,19 @@ public class PlayerRepository(ApplicationContext context, IMapper mapper, ILogge
})
.Select(p => new PlayerMvpDto()
{
PlayerName = p.PlayerName,
Rank = p.Rank,
TotalVsDuelPoints = p.VsDuels,
MarshalGuardParticipationCount = p.MarshalGuardParticipationCount,
Name = p.PlayerName,
AllianceRank = p.Rank,
DuelPointsLast3Weeks = p.VsDuels,
MarshalParticipationCount = p.MarshalGuardParticipationCount,
DesertStormParticipationCount = p.DessertStormParticipationCount,
MvpPoints = Math.Round(
MvpScore = Math.Round(
(decimal)((p.VsDuels / 1000000.0 * 0.8) +
((p.MarshalGuardParticipationCount * 20 + p.DessertStormParticipationCount * 40) * 0.2)), 2)
})
.OrderByDescending(p => p.MvpPoints)
.ThenByDescending(p => p.TotalVsDuelPoints)
.ThenByDescending(p => p.MarshalGuardParticipationCount)
.ThenBy(p => p.PlayerName)
.OrderByDescending(p => p.MvpScore)
.ThenByDescending(p => p.DuelPointsLast3Weeks)
.ThenByDescending(p => p.MarshalParticipationCount)
.ThenBy(p => p.Name)
.ToListAsync(cancellationToken);
return playerMvps;
@ -176,8 +237,20 @@ public class PlayerRepository(ApplicationContext context, IMapper mapper, ILogge
public async Task<Result<PlayerDto>> CreatePlayerAsync(CreatePlayerDto createPlayerDto, string createdBy, CancellationToken cancellationToken)
{
var newPlayer = mapper.Map<Player>(createPlayerDto);
newPlayer.CreatedBy = createdBy;
var newPlayer = new Player()
{
CreatedBy = createdBy,
PlayerName = createPlayerDto.PlayerName,
AllianceId = createPlayerDto.AllianceId,
RankId = createPlayerDto.RankId,
Level = createPlayerDto.Level,
CreatedOn = DateTime.Now,
ModifiedOn = null,
ModifiedBy = null,
DismissalReason = null,
DismissedAt = null,
IsDismissed = false
};
await context.Players.AddAsync(newPlayer, cancellationToken);

View File

@ -42,6 +42,8 @@ public class ApplicationContext(DbContextOptions<ApplicationContext> options) :
public DbSet<VsDuelLeague> VsDuelLeagues { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);

View File

@ -0,0 +1,25 @@
using Database.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Database.Configurations;
public class ApiKeyConfiguration : IEntityTypeConfiguration<ApiKey>
{
public void Configure(EntityTypeBuilder<ApiKey> builder)
{
builder.HasKey(apiKey => apiKey.Id);
builder.Property(apiKey => apiKey.Id).ValueGeneratedNever();
builder.Property(apiKey => apiKey.EncryptedKey).IsRequired().HasMaxLength(64);
builder.Property(apiKey => apiKey.CreatedOn).IsRequired();
builder.Property(apiKey => apiKey.CreatedBy).IsRequired().HasMaxLength(150);
builder.Property(apiKey => apiKey.ModifiedOn).IsRequired(false);
builder.Property(apiKey => apiKey.ModifiedBy).IsRequired(false);
builder.HasOne(apiKey => apiKey.Alliance)
.WithOne(alliance => alliance.ApiKey)
.HasForeignKey<ApiKey>(apiKey => apiKey.AllianceId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@ -14,6 +14,8 @@ public class Alliance : BaseEntity
public string? ModifiedBy { get; set; }
public ApiKey? ApiKey { get; set; }
public ICollection<Player> Players { get; set; } = [];
public ICollection<User> Users { get; set; } = [];

View File

@ -0,0 +1,18 @@
namespace Database.Entities;
public class ApiKey : BaseEntity
{
public Alliance Alliance { get; set; } = null!;
public Guid AllianceId { get; set; }
public required string EncryptedKey { get; set; }
public DateTime CreatedOn { get; set; }
public required string CreatedBy { get; set; }
public DateTime? ModifiedOn { get; set; }
public string? ModifiedBy { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Database.Migrations
{
/// <inheritdoc />
public partial class AddApiKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ApiKeys",
schema: "dbo",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
AllianceId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
EncryptedKey = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CreatedOn = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false),
ModifiedOn = table.Column<DateTime>(type: "datetime2", nullable: true),
ModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiKeys", x => x.Id);
table.ForeignKey(
name: "FK_ApiKeys_Alliances_AllianceId",
column: x => x.AllianceId,
principalSchema: "dbo",
principalTable: "Alliances",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ApiKeys_AllianceId",
schema: "dbo",
table: "ApiKeys",
column: "AllianceId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApiKeys",
schema: "dbo");
}
}
}

View File

@ -91,6 +91,41 @@ namespace Database.Migrations
b.ToTable("Alliances", "dbo");
});
modelBuilder.Entity("Database.Entities.ApiKey", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AllianceId")
.HasColumnType("uniqueidentifier");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.Property<DateTime>("CreatedOn")
.HasColumnType("datetime2");
b.Property<string>("EncryptedKey")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("ModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("ModifiedOn")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("AllianceId")
.IsUnique();
b.ToTable("ApiKeys", "dbo");
});
modelBuilder.Entity("Database.Entities.CustomEvent", b =>
{
b.Property<Guid>("Id")
@ -588,19 +623,19 @@ namespace Database.Migrations
b.HasData(
new
{
Id = new Guid("01946f11-c5f1-771e-8600-331582290457"),
Id = new Guid("0194d053-12b9-7818-9417-4d4eaa3b5ec1"),
Code = 1,
Name = "Silver League"
},
new
{
Id = new Guid("01946f11-c5f1-7576-b861-14df423f92f2"),
Id = new Guid("0194d053-12b9-7c64-9efb-e88745510c35"),
Code = 2,
Name = "Gold League"
},
new
{
Id = new Guid("01946f11-c5f1-750f-b3f5-61ec7a00f837"),
Id = new Guid("0194d053-12b9-7217-a040-88ba1bbc7d69"),
Code = 3,
Name = "Diamond League"
});
@ -856,6 +891,17 @@ namespace Database.Migrations
b.Navigation("Player");
});
modelBuilder.Entity("Database.Entities.ApiKey", b =>
{
b.HasOne("Database.Entities.Alliance", "Alliance")
.WithOne("ApiKey")
.HasForeignKey("Database.Entities.ApiKey", "AllianceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Alliance");
});
modelBuilder.Entity("Database.Entities.CustomEvent", b =>
{
b.HasOne("Database.Entities.Alliance", "Alliance")
@ -1107,6 +1153,8 @@ namespace Database.Migrations
modelBuilder.Entity("Database.Entities.Alliance", b =>
{
b.Navigation("ApiKey");
b.Navigation("CustomEvents");
b.Navigation("DesertStorms");

View File

@ -6,6 +6,17 @@ This project is currently in the **Beta Phase**.
---
### **[0.7.0]** - *2025-02-06*
#### ✨ Added
- **MVP Page**: A new page has been introduced where players can be loaded and a list of MVPs is displayed based on a calculation formula.
- **Filter Options**: You can now display the entire alliance, only R4/R5 members, or just players without R5/R4.
- **API Key**: A new tab has been added under the alliance section, allowing users to generate an API key to access endpoints and integrate them into their own systems.
- **MVP List Endpoint**: The API endpoint for fetching the MVP list is now available.
#### 🛠️ Fixed
- *(N/A)*
### **[0.6.1]** - *2025-01-28*
#### ✨ Added
- *(N/A)*

View File

@ -23,12 +23,14 @@ import {ZombieSiegeComponent} from "./pages/zombie-siege/zombie-siege.component"
import {ZombieSiegeDetailComponent} from "./pages/zombie-siege/zombie-siege-detail/zombie-siege-detail.component";
import {CustomEventDetailComponent} from "./pages/custom-event/custom-event-detail/custom-event-detail.component";
import {DismissPlayerComponent} from "./pages/dismiss-player/dismiss-player.component";
import {MvpComponent} from "./pages/mvp/mvp.component";
const routes: Routes = [
{path: 'players', component: PlayerComponent, canActivate: [authGuard]},
{path: 'dismiss-players', component: DismissPlayerComponent, canActivate: [authGuard]},
{path: 'player-information/:id', component: PlayerInformationComponent, canActivate: [authGuard]},
{path: 'marshal-guard', component: MarshalGuardComponent, canActivate: [authGuard]},
{path: 'mvp', component: MvpComponent, canActivate: [authGuard]},
{path: 'marshal-guard-detail/:id', component: MarshalGuardDetailComponent, canActivate: [authGuard]},
{path: 'vs-duel', component: VsDuelComponent, canActivate: [authGuard]},
{path: 'vs-duel-detail/:id', component: VsDuelDetailComponent, canActivate: [authGuard]},

View File

@ -56,6 +56,9 @@ import { PlayerDismissInformationModalComponent } from './modals/player-dismiss-
import { PlayerExcelImportModalComponent } from './modals/player-excel-import-modal/player-excel-import-modal.component';
import {AgCharts} from "ag-charts-angular";
import {PlayerInfoVsDuelComponent} from "./pages/player-information/player-info-vs-duel/player-info-vs-duel.component";
import { MvpComponent } from './pages/mvp/mvp.component';
import { AllianceApiKeyComponent } from './pages/alliance/alliance-api-key/alliance-api-key.component';
import { AllianceUserAdministrationComponent } from './pages/alliance/alliance-user-administration/alliance-user-administration.component';
@NgModule({
declarations: [
@ -101,7 +104,10 @@ import {PlayerInfoVsDuelComponent} from "./pages/player-information/player-info-
CustomEventDetailComponent,
DismissPlayerComponent,
PlayerDismissInformationModalComponent,
PlayerExcelImportModalComponent
PlayerExcelImportModalComponent,
MvpComponent,
AllianceApiKeyComponent,
AllianceUserAdministrationComponent
],
imports: [
BrowserModule,

View File

@ -0,0 +1,18 @@
export interface ApiKeyModel {
id: string;
allianceId: string;
key: string;
createdOn: Date;
createdBy: string;
modifiedOn?: Date;
modifiedBy?: string;
}
export interface CreateApiKeyModel {
allianceId: string;
}
export interface UpdateApiKeyModel {
id: string;
allianceId: string;
}

View File

@ -1,8 +1,6 @@
import {NoteModel} from "./note.model";
import {AdmonitionModel} from "./admonition.model";
import {DesertStormParticipantModel} from "./desertStormParticipant.model";
import {MarshalGuardParticipantModel} from "./marshalGuardParticipant.model";
import {VsDuelParticipantModel} from "./vsDuelParticipant.model";
export interface PlayerModel {
id: string;
@ -22,6 +20,16 @@ export interface PlayerModel {
dismissalReason?: string;
}
export interface PlayerMvpModel {
name: string;
allianceRank: string;
duelPointsLast3Weeks: number;
marshalParticipationCount: number;
desertStormParticipationCount: number;
hasParticipatedInOldestDuel: boolean;
mvpScore: number;
}
export interface CreatePlayerModel {
playerName: string;
rankId: string;

View File

@ -30,6 +30,11 @@
</a>
</li>
<li class="nav-item">
<a (click)="isShown = false" class="nav-link" routerLink="/mvp" routerLinkActive="active">MVP
</a>
</li>
<li class="nav-item">
<a (click)="isShown = false" class="nav-link" routerLink="/marshal-guard" routerLinkActive="active">Marshal Guard
</a>

View File

@ -0,0 +1,90 @@
.api-key-card {
max-width: 800px;
margin-left: auto;
margin-right: auto;
margin-top: 24px;
border: none;
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
.card-header {
border-top-left-radius: 15px;
border-top-right-radius: 15px;
font-size: 1.5rem;
background: linear-gradient(45deg, #007bff, #0056b3);
}
}
.api-key-input-group {
max-width: 420px; // Begrenzung der Breite des Input-Feldes
}
.api-key-input {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group-append .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.no-api-key-card {
max-width: 400px;
margin: 24px auto;
padding: 20px;
border: none;
border-radius: 15px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #3a3a3a;
}
.no-api-key-icon {
font-size: 3rem;
color: #007bff;
}
.btn-primary {
border-radius: 25px;
padding: 10px 20px;
font-weight: bold;
}
h2 {
color: #ffffff; /* Weiße Schriftfarbe für Überschriften */
}
h4, h5 {
color: #f1f1f1; /* Hellere Farbe für Unterüberschriften */
}
.alert-info {
background-color: #3a3a3a; /* Dunklerer Hintergrund für Alerts */
border-color: #555555;
color: #a6c8ff; /* Hellere Schriftfarbe im Alert */
}
.alert-heading {
color: #ffffff; /* Weiße Schrift für den Alert-Header */
}
pre {
background-color: #2d2d2d; /* Dunkles Hintergrund für Code-Block */
border-radius: 5px;
padding: 15px;
overflow-x: auto;
color: #f1f1f1; /* Heller Text im Code */
border: 1px solid #444444; /* Dünner Rand um den Code-Block */
}
code {
font-family: Consolas, Monaco, monospace;
color: #ffcc00; /* Helle Farbe für den Code */
}
a {
color: #66b2ff; /* Blaue Farbe für Links */
}
a:hover {
color: #3388cc; /* Dunkleres Blau beim Hover über Links */
}

View File

@ -0,0 +1,131 @@
<h2 class="text-center mb-4">API Key Generation and Usage</h2>
@if (apiKey) {
<div class="card mb-3 api-key-card">
<div class="card-header bg-gradient-primary text-white">
API Key Details
</div>
<div class="card-body">
<!-- API Key Field mit Icon Toggle und Kopierfunktion -->
<div class="form-group">
<label for="apiKey">API Key</label>
<div class="input-group api-key-input-group">
<input
[type]="isKeyVisible ? 'text' : 'password'"
class="form-control api-key-input"
id="apiKey"
[value]="apiKey.key"
readonly>
<div class="input-group-append">
<button
class="btn btn-outline-secondary"
type="button"
(click)="toggleKeyVisibility()">
<i [ngClass]="isKeyVisible ? 'bi bi-eye-slash' : 'bi bi-eye'"></i>
</button>
<button
class="btn btn-outline-secondary"
type="button"
(click)="copyApiKey()"
[attr.title]="copied ? 'Copied!' : 'Copy to clipboard'">
<i [ngClass]="copied ? 'bi bi-check-lg text-success' : 'bi bi-clipboard'"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label for="allianceId">Alliance Id</label>
<div class="input-group api-key-input-group">
<input
[type]="isIdVisible ? 'text' : 'password'"
class="form-control api-key-input"
id="allianceId"
[value]="allianceId"
readonly>
<div class="input-group-append">
<button
class="btn btn-outline-secondary"
type="button"
(click)="toggleIdVisibility()">
<i [ngClass]="isIdVisible ? 'bi bi-eye-slash' : 'bi bi-eye'"></i>
</button>
<button
class="btn btn-outline-secondary"
type="button"
(click)="copyAllianceId()"
[attr.title]="copied ? 'Copied!' : 'Copy to clipboard'">
<i [ngClass]="copied ? 'bi bi-check-lg text-success' : 'bi bi-clipboard'"></i>
</button>
</div>
</div>
</div>
<div class="metadata mt-4">
<p><strong>Created on:</strong> {{ apiKey.createdOn | date: 'dd.MM.yyyy HH:mm' }}</p>
<p><strong>Created by:</strong> {{ apiKey.createdBy }}</p>
@if (apiKey.modifiedBy) {
<p><strong>Last updated on:</strong> {{ apiKey.modifiedOn | date: 'dd.MM.yyyy HH:mm' }}</p>
<p><strong>Last updated by:</strong> {{ apiKey.modifiedBy }}</p>
}
</div>
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-primary" (click)="regenerateApiKey(apiKey.id)">
<i class="bi bi-arrow-clockwise"></i>
Refresh Key
</button>
<button class="btn btn-danger" (click)="deleteApiKey(apiKey.id)">
<i class="bi bi-trash"></i>
Delete Key
</button>
</div>
</div>
</div>
} @else {
<div class="card text-center no-api-key-card">
<div class="card-body">
<i class="bi bi-key-fill no-api-key-icon"></i>
<h5 class="card-title mt-3">No API Key Available</h5>
<p class="card-text">
You have not created an API Key yet. Click the button below to generate one.
</p>
<button class="btn btn-primary" (click)="generateApiKey()">
<i class="bi bi-plus-lg"></i> Generate API Key
</button>
</div>
</div>
}
<div class="container mt-5 pb-5">
<div class="row">
<div class="col-md-12">
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">Important Information</h4>
<p>To interact with our system's endpoints, you need to generate an <strong>API Key</strong>. The API Key serves
as an authentication token for your requests, ensuring that only authorized users can access the available
endpoints.</p>
</div>
<h4>How to Use the API Key</h4>
<p>You can send the API Key in two ways:</p>
<ul>
<li><strong>As a URL Query Parameter:</strong> Add the <code>key</code> parameter to your URL.</li>
<li><strong>As a Request Header:</strong> Include the API Key in the <code>X-Api-Key</code> header.</li>
</ul>
<h5>Examples</h5>
<pre><code>https://api.example.com/data?key=YOUR_API_KEY&allianceId=YOUR_ALLIANCE_ID</code></pre>
<pre><code>curl -X GET "https://api.example.com/data?key=YOUR_API_KEY&allianceId=12345"</code></pre>
<p>In both cases, make sure to include your <strong>AllianceId</strong> as a query parameter to ensure correct
access and data retrieval.</p>
<h4>Swagger Documentation</h4>
<p>To explore all available API endpoints and their detailed usage, check out our <a
href="https://player-manager.last-war.ch/swagger" target="_blank">Swagger documentation here</a>.</p>
</div>
</div>
</div>

View File

@ -0,0 +1,161 @@
import {Component, inject, Input, OnInit} from '@angular/core';
import Swal from "sweetalert2";
import {ApiKeyModel, CreateApiKeyModel, UpdateApiKeyModel} from "../../../models/apiKey.model";
import {ApiKeyService} from "../../../services/api-key.service";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-alliance-api-key',
templateUrl: './alliance-api-key.component.html',
styleUrl: './alliance-api-key.component.css'
})
export class AllianceApiKeyComponent implements OnInit {
private readonly _apiKeyService: ApiKeyService = inject(ApiKeyService);
private readonly _toastr: ToastrService = inject(ToastrService);
@Input('allianceId') allianceId!: string;
public isKeyVisible: any;
public copied = false;
public isIdVisible: boolean = false;
public apiKey: ApiKeyModel | undefined;
ngOnInit() {
this.getAllianceApiKey(this.allianceId);
}
toggleKeyVisibility() {
this.isKeyVisible = !this.isKeyVisible;
}
getAllianceApiKey(allianceId: string) {
this._apiKeyService.getAllianceApiKey(allianceId).subscribe({
next: ((response) => {
if (response) {
this.apiKey = response;
}
}),
error: (error) => {
this.apiKey = undefined;
console.log(error);
}
})
}
regenerateApiKey(apiKeyId: string) {
Swal.fire({
title: "Regenerate API Key?",
text: "Are you sure you want to generate a new API Key? Your current key will be invalidated, and you will need to use the new key for API access.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, regenerate it!"
}).then((result) => {
if (result.isConfirmed) {
const updateApiKey: UpdateApiKeyModel = {
allianceId: this.allianceId!,
id: apiKeyId
};
this._apiKeyService.updateApiKey(apiKeyId, updateApiKey).subscribe({
next: ((response) => {
if (response) {
Swal.fire({
title: "API Key Regenerated!",
text: "A new API Key has been successfully generated. Please update your applications with the new key.",
icon: "success",
}).then(_ => this.getAllianceApiKey(this.allianceId!));
}
}),
error: (error: Error) => {
console.log(error);
Swal.fire({
title: "Error!",
text: "Something went wrong while regenerating the API Key. Please try again later.",
icon: "error",
}).then();
}
});
}
});
}
deleteApiKey(apiKeyId: string) {
Swal.fire({
title: "Delete API Key ?",
text: "Are you sure you want to delete this API Key? Once deleted, you will no longer be able to access the API without a new key.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, I understand delete it!"
}).then((result) => {
if (result.isConfirmed) {
this._apiKeyService.deleteApiKey(apiKeyId).subscribe({
next: ((response) => {
if (response) {
Swal.fire({
title: "Deleted!",
text: "The API Key has been successfully deleted. You will need to generate a new key to access the API.",
icon: "success",
}).then(_ => this.getAllianceApiKey(this.allianceId!));
}
}),
error: (error: Error) => {
console.log(error);
}
});
}
});
}
generateApiKey() {
const createApiKeyModel: CreateApiKeyModel = {
allianceId: this.allianceId!,
}
this._apiKeyService.createApiKey(createApiKeyModel).subscribe({
next: ((response) => {
if (response) {
this._toastr.success('Successfully generated api key!');
this.apiKey = response;
}
}),
error: (error: Error) => {
console.log(error);
this._toastr.error('Failed to generate api key', 'Generate api key');
}
})
}
toggleIdVisibility() {
this.isIdVisible = !this.isIdVisible;
}
copyAllianceId() {
navigator.clipboard.writeText(this.allianceId!)
.then(() => {
this.copied = true;
this._toastr.info('Alliance id copied to clipboard!');
setTimeout(() => this.copied = false, 2000);
})
.catch(err => {
console.error(err);
this._toastr.error('Failed to copy Alliance id.');
});
}
copyApiKey(): void {
navigator.clipboard.writeText(this.apiKey!.key)
.then(() => {
this.copied = true;
this._toastr.info('API key copied to clipboard!');
setTimeout(() => this.copied = false, 2000);
})
.catch(err => {
console.error(err);
this._toastr.error('Failed to copy API key.');
});
}
}

View File

@ -0,0 +1,34 @@
<div class="d-grid gap-2 col-6 mx-auto mt-3">
<button (click)="onInviteUser()" class="btn btn-primary" type="button">Invite user</button>
</div>
<div class="table-responsive mt-3">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Role</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
@for (user of users | paginate: {itemsPerPage: 10, currentPage: page, id: 'userTable'}; track user.id) {
<tr>
<td>{{ user.playerName }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
<td>
<div class="d-flex justify-content-around">
<i ngbTooltip="Edit" placement="auto" (click)="onEditUser(user)"
class="bi custom-edit-icon bi-pencil-fill"></i>
<i ngbTooltip="Delete" placement="auto" (click)="onDeleteUser(user)"
class="bi custom-delete-icon bi-trash3"></i>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<pagination-controls (pageChange)="page = $event" [id]="'userTable'" [responsive]="true"
class="custom-pagination"></pagination-controls>

View File

@ -0,0 +1,100 @@
import {Component, inject, Input, OnInit} from '@angular/core';
import {UserModel} from "../../../models/user.model";
import {UserEditModalComponent} from "../../../modals/user-edit-modal/user-edit-modal.component";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {UserService} from "../../../services/user.service";
import {ToastrService} from "ngx-toastr";
import Swal from "sweetalert2";
import {InviteUserModalComponent} from "../../../modals/invite-user-modal/invite-user-modal.component";
import {JwtTokenService} from "../../../services/jwt-token.service";
@Component({
selector: 'app-alliance-user-administration',
templateUrl: './alliance-user-administration.component.html',
styleUrl: './alliance-user-administration.component.css'
})
export class AllianceUserAdministrationComponent implements OnInit {
public users: UserModel[] = [];
public page: number = 1;
@Input('allianceId') allianceId!: string;
private readonly _modalService: NgbModal = inject(NgbModal);
private readonly _userService: UserService = inject(UserService);
private readonly _toastr: ToastrService = inject(ToastrService);
private readonly _tokenService: JwtTokenService = inject(JwtTokenService);
ngOnInit() {
this.getAllianceUsers(this.allianceId);
}
onInviteUser() {
const modalRef = this._modalService.open(InviteUserModalComponent,
{animation: true, backdrop: 'static', centered: true, size: 'lg'});
modalRef.componentInstance.userId = this._tokenService.getUserId();
modalRef.componentInstance.allianceId = this.allianceId;
modalRef.closed.subscribe({
next: ((response) => {
if (response) {
}
})
})
}
getAllianceUsers(allianceId: string) {
this._userService.getAllianceUsers(allianceId).subscribe({
next: ((response) => {
if (response) {
this.users = response;
} else {
this.users = [];
}
}),
error: ((error) => {
console.log(error);
this._toastr.error('Could not load alliance users', 'Error load users');
})
})
}
onEditUser(user: UserModel) {
const modalRef = this._modalService.open(UserEditModalComponent,
{animation: true, backdrop: 'static', centered: true, size: 'lg'});
modalRef.componentInstance.currentUser = user;
modalRef.closed.subscribe({
next: ((response: UserModel) => {
if (response) {
this.getAllianceUsers(this.allianceId!);
}
})
})
}
onDeleteUser(user: UserModel) {
Swal.fire({
title: "Delete User ?",
text: `Do you really want to delete the user ${user.playerName}`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete it!"
}).then((result) => {
if (result.isConfirmed) {
this._userService.deleteUser(user.id).subscribe({
next: ((response) => {
if (response) {
Swal.fire({
title: "Deleted!",
text: "User has been deleted",
icon: "success"
}).then(_ => this.getAllianceUsers(this.allianceId!));
}
}),
error: (error: Error) => {
console.log(error);
}
});
}
});
}
}

View File

@ -1,15 +1,17 @@
<div class="container mt-3">
<h2 class="text-center">Alliance</h2>
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs">
<ul #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" ngbNav>
<li [ngbNavItem]="1">
<button ngbNavLink>Alliance data</button>
<ng-template ngbNavContent>
@if (allianceForm) {
<div class="mt-3">
<div class="alert alert-secondary d-flex justify-content-between" role="alert">
<div>Created: <span class="text-primary">{{ currentAlliance?.createdOn | date: 'dd.MM.yyyy' }}</span></div>
<div>Created: <span class="text-primary">{{ currentAlliance?.createdOn | date: 'dd.MM.yyyy' }}</span>
</div>
@if (currentAlliance?.modifiedOn) {
<div>Modified: <span class="text-primary">{{ currentAlliance?.modifiedOn | date: 'dd.MM.yyyy HH:mm' }}</span>
<div>Modified: <span
class="text-primary">{{ currentAlliance?.modifiedOn | date: 'dd.MM.yyyy HH:mm' }}</span>
by <span class="text-primary">{{ currentAlliance?.modifiedBy }}</span></div>
}
</div>
@ -67,7 +69,8 @@
}
</div>
<div class="d-grid gap-2 col-6 mx-auto">
<button [disabled]="allianceForm.invalid || !allianceForm.dirty" class="btn btn-success" type="submit">Update
<button [disabled]="allianceForm.invalid || !allianceForm.dirty" class="btn btn-success" type="submit">
Update
</button>
</div>
</form>
@ -77,42 +80,23 @@
<li [ngbNavItem]="2">
<button ngbNavLink>User administration</button>
<ng-template ngbNavContent>
<div class="d-grid gap-2 col-6 mx-auto mt-3">
<button (click)="onInviteUser()" class="btn btn-primary" type="button">Invite user</button>
</div>
<div class="table-responsive mt-3">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Role</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
@for (user of users | paginate: { itemsPerPage: 10, currentPage: page, id: 'userTable'}; track user.id) {
<tr>
<td>{{user.playerName}}</td>
<td>{{user.email}}</td>
<td>{{user.role}}</td>
<td>
<div class="d-flex justify-content-around">
<i ngbTooltip="Edit" placement="auto" (click)="onEditUser(user)" class="bi custom-edit-icon bi-pencil-fill"></i>
<i ngbTooltip="Delete" placement="auto" (click)="onDeleteUser(user)" class="bi custom-delete-icon bi-trash3"></i>
</div>
</td>
</tr>
@if (activeTab === 2) {
<ng-container>
<app-alliance-user-administration [allianceId]="allianceId!"></app-alliance-user-administration>
</ng-container>
}
</ng-template>
</li>
<li [ngbNavItem]="3">
<button ngbNavLink>API Key</button>
<ng-template ngbNavContent>
@if (activeTab === 3) {
<ng-container>
<app-alliance-api-key [allianceId]="allianceId!"></app-alliance-api-key>
</ng-container>
}
</tbody>
</table>
</div>
<pagination-controls class="custom-pagination" [responsive]="true" [id]="'userTable'" (pageChange)="page = $event"></pagination-controls>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="mt-2"></div>
</div>

View File

@ -4,12 +4,7 @@ import {AllianceModel} from "../../models/alliance.model";
import {ToastrService} from "ngx-toastr";
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {JwtTokenService} from "../../services/jwt-token.service";
import {UserModel} from "../../models/user.model";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {InviteUserModalComponent} from "../../modals/invite-user-modal/invite-user-modal.component";
import {UserService} from "../../services/user.service";
import {UserEditModalComponent} from "../../modals/user-edit-modal/user-edit-modal.component";
import Swal from "sweetalert2";
@Component({
selector: 'app-alliance',
@ -22,16 +17,13 @@ export class AllianceComponent implements OnInit {
private readonly _allianceService: AllianceService = inject(AllianceService);
private readonly _toastr: ToastrService = inject(ToastrService);
private readonly _tokenService: JwtTokenService = inject(JwtTokenService);
private readonly _modalService : NgbModal = inject(NgbModal);
private readonly _userService: UserService = inject(UserService);
private allianceId = this._tokenService.getAllianceId();
public allianceId = this._tokenService.getAllianceId();
public allianceForm: FormGroup | undefined;
public currentAlliance: AllianceModel | undefined;
active: number = 1;
public users: UserModel[] = [];
page: number = 1;
public activeTab: number = 1;
get f() {
return this.allianceForm!.controls;
@ -39,7 +31,6 @@ export class AllianceComponent implements OnInit {
ngOnInit() {
this.getAlliance(this.allianceId!);
this.getAllianceUsers(this.allianceId!);
}
getAlliance(allianceId: string) {
@ -57,22 +48,6 @@ export class AllianceComponent implements OnInit {
});
}
getAllianceUsers(allianceId: string) {
this._userService.getAllianceUsers(allianceId).subscribe({
next: ((response) => {
if (response) {
this.users = response;
} else {
this.users = [];
}
}),
error: ((error) => {
console.log(error);
this._toastr.error('Could not load alliance users', 'Error load users');
})
})
}
createAllianceForm(alliance: AllianceModel) {
this.allianceForm = new FormGroup({
id: new FormControl<string>(alliance.id),
@ -105,58 +80,4 @@ export class AllianceComponent implements OnInit {
});
}
onEditUser(user: UserModel) {
const modalRef = this._modalService.open(UserEditModalComponent,
{animation: true, backdrop: 'static', centered: true, size: 'lg'});
modalRef.componentInstance.currentUser = user;
modalRef.closed.subscribe({
next: ((response: UserModel) => {
if (response) {
this.getAllianceUsers(this.allianceId!);
}
})
})
}
onDeleteUser(user: UserModel) {
Swal.fire({
title: "Delete User ?",
text: `Do you really want to delete the user ${user.playerName}`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete it!"
}).then((result) => {
if (result.isConfirmed) {
this._userService.deleteUser(user.id).subscribe({
next: ((response) => {
if (response) {
Swal.fire({
title: "Deleted!",
text: "User has been deleted",
icon: "success"
}).then(_ => this.getAllianceUsers(this.allianceId!));
}
}),
error: (error: Error) => {
console.log(error);
}
});
}
});
}
onInviteUser() {
const modalRef = this._modalService.open(InviteUserModalComponent,
{animation: true, backdrop: 'static', centered: true, size: 'lg'});
modalRef.componentInstance.userId = this._tokenService.getUserId();
modalRef.componentInstance.allianceId = this.allianceId;
modalRef.closed.subscribe({
next: ((response) => {
if (response) {
}
})
})
}
}

View File

View File

@ -0,0 +1,134 @@
<div class="container mt-3 pb-5">
<h2 class="text-center mb-4">🏆 MVP Leaderboard</h2>
<div class="container mt-4">
<!-- Info-Toggle Button -->
<div class="d-flex justify-content-center mb-3">
<button class="btn btn-info" type="button" (click)="toggleInfo()">
{{ showInfo ? 'Hide' : 'Show' }} How the MVP List is Generated
</button>
</div>
@if (showInfo) {
<div class="alert alert-light border p-3">
<h5>📊 How the MVP List is Generated</h5>
<p>
The MVP list is generated based on each player's activity in the last three weeks.
The calculation takes into account <strong>Weekly VS Duel Points</strong>,
<strong>Marshal Participation</strong>, and <strong>Desert Storm Participation</strong>.
</p>
<h6>🔹 Step 1: Weekly VS Duel Points (80% Weighting)</h6>
<p>
This is the most important factor. The total VS points from the last three weeks are divided by 1,000,000 and multiplied by 0.8.
</p>
<p><strong>Formula:</strong></p>
<code>(Total VS Points from last 3 weeks / 1,000,000) * 0.8</code>
<h6>🔹 Step 2: Participation in Events (20% Weighting)</h6>
<ul>
<li><strong>Marshal Participation:</strong> Each participation earns 20 points.</li>
<li><strong>Desert Storm Participation:</strong> Each participation earns 40 points.</li>
</ul>
<p><strong>Formula:</strong></p>
<code>(Marshal points + Desert Storm points) * 0.2</code>
<h5>📌 Example Calculation</h5>
<ul>
<li><strong>VS Points from last 3 weeks:</strong> 130 million</li>
<li><strong>Marshal Participations:</strong> 8</li>
<li><strong>Desert Storm Participations:</strong> 2</li>
</ul>
<p>Final Calculation:</p>
<ul>
<li>VS Points: <code>(130,000,000 / 1,000,000) * 0.8 = 104</code></li>
<li>Marshal: <code>8 * 20 = 160</code></li>
<li>Desert Storm: <code>2 * 40 = 80</code></li>
<li>Additional Points: <code>(160 + 80) * 0.2 = 48</code></li>
<li><strong>Total MVP Points: 104 + 48 = 152</strong></li>
</ul>
<h5>🔍 How the MVP List Works</h5>
<p>
The system ranks players based on their total MVP points. You can filter the list using the options below:
</p>
<ul>
<li><strong>Players Only:</strong> Show only regular players.</li>
<li><strong>Only R5/R4:</strong> Show only high-ranking players.</li>
<li><strong>Show All:</strong> Display the complete MVP list.</li>
</ul>
</div>
}
<div class="d-flex justify-content-center">
<div class="btn-group" role="group" aria-label="MVP Filter">
<input
type="radio"
class="btn-check"
name="mvpFilter"
id="playersOnly"
value="players"
[(ngModel)]="selectedFilter"
(change)="onSelectChange()"
/>
<label class="btn btn-outline-primary" for="playersOnly">Players Only</label>
<input
type="radio"
class="btn-check"
name="mvpFilter"
id="leadership"
value="leadership"
[(ngModel)]="selectedFilter"
(change)="onSelectChange()"
/>
<label class="btn btn-outline-primary" for="leadership">Only R5/R4</label>
<input
type="radio"
class="btn-check"
name="mvpFilter"
id="all"
value="all"
[(ngModel)]="selectedFilter"
(change)="onSelectChange()"
checked
/>
<label class="btn btn-outline-primary" for="all">Show All</label>
</div>
</div>
<div class="table-responsive mt-5">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Rank</th>
<th scope="col">VS Points</th>
<th scope="col">Marshal Participation</th>
<th scope="col">Desert Storm Participation</th>
<th scope="col">MVP Points</th>
<th scope="col">Attended 3 VS Duel weeks</th>
</tr>
</thead>
<tbody>
@for (player of mvpPlayers; track player.name; let i = $index) {
<tr>
<td>{{i + 1}}</td>
<td>{{player.name}}</td>
<td>{{player.allianceRank}}</td>
<td>{{player.duelPointsLast3Weeks | number}}</td>
<td>{{player.marshalParticipationCount}}</td>
<td>{{player.desertStormParticipationCount}}</td>
<td>{{player.mvpScore}}</td>
<td class="text-center"><i class="bi" [ngClass]="player.hasParticipatedInOldestDuel ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'"></i>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,54 @@
import {Component, inject, OnInit} from '@angular/core';
import {PlayerService} from "../../services/player.service";
import {JwtTokenService} from "../../services/jwt-token.service";
import {ToastrService} from "ngx-toastr";
import {PlayerMvpModel} from "../../models/player.model";
@Component({
selector: 'app-mvp',
templateUrl: './mvp.component.html',
styleUrl: './mvp.component.css'
})
export class MvpComponent implements OnInit {
private readonly _playerService: PlayerService = inject(PlayerService);
private readonly _jwtTokenService: JwtTokenService = inject(JwtTokenService);
private readonly _toastr: ToastrService = inject(ToastrService);
private readonly _allianceId: string = this._jwtTokenService.getAllianceId()!;
public mvpPlayers: PlayerMvpModel[] = [];
selectedFilter: string = 'players';
showInfo: boolean = false;
ngOnInit() {
this.getMvpPlayerList(this.selectedFilter);
}
getMvpPlayerList(playerType: string) {
this._playerService.getAllianceMvpPlayers(this._allianceId, playerType).subscribe({
next: ((response) => {
if (response) {
this.mvpPlayers = response;
} else {
this.mvpPlayers = [];
}
}),
error: (error) => {
this.mvpPlayers = [];
console.error(error);
this._toastr.error('Error getting mvp players', 'Error');
}
});
}
onSelectChange() {
this.getMvpPlayerList(this.selectedFilter);
}
toggleInfo() {
this.showInfo = !this.showInfo;
}
}

View File

@ -0,0 +1,30 @@
import {inject, Injectable} from '@angular/core';
import {environment} from "../../environments/environment";
import {HttpClient} from "@angular/common/http";
import {ApiKeyModel, CreateApiKeyModel, UpdateApiKeyModel} from "../models/apiKey.model";
import {Observable} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class ApiKeyService {
private readonly _serviceUrl = environment.apiBaseUrl + 'ApiKeys/';
private readonly _httpClient: HttpClient = inject(HttpClient);
getAllianceApiKey(apiKeyId: string): Observable<ApiKeyModel> {
return this._httpClient.get<ApiKeyModel>(this._serviceUrl + apiKeyId);
}
createApiKey(createApiKey: CreateApiKeyModel): Observable<ApiKeyModel> {
return this._httpClient.post<ApiKeyModel>(this._serviceUrl, createApiKey);
}
updateApiKey(apiKeyId: string, updateApiKey: UpdateApiKeyModel): Observable<ApiKeyModel> {
return this._httpClient.put<ApiKeyModel>(this._serviceUrl + apiKeyId, updateApiKey);
}
deleteApiKey(apiKeyId: string): Observable<boolean> {
return this._httpClient.delete<boolean>(this._serviceUrl + apiKeyId);
}
}

View File

@ -5,7 +5,7 @@ import {Observable} from "rxjs";
import {
CreatePlayerModel,
DismissPlayerInformationModel,
PlayerModel,
PlayerModel, PlayerMvpModel,
UpdatePlayerModel
} from "../models/player.model";
import {ExcelImportResponseModel} from "../models/excelImportResponse.model";
@ -38,6 +38,13 @@ export class PlayerService {
return this._httpClient.get<DismissPlayerInformationModel>(this._serviceUrl + 'DismissInformation/' + playerId);
}
public getAllianceMvpPlayers(allianceId: string, playerType: string): Observable<PlayerMvpModel[]> {
let params = new HttpParams();
params = params.append("allianceId", allianceId);
params = params.append("playerType", playerType);
return this._httpClient.get<PlayerMvpModel[]>(this._serviceUrl + 'Mvp', {params: params});
}
public updatePlayer(playerId: string, player: UpdatePlayerModel): Observable<PlayerModel> {
return this._httpClient.put<PlayerModel>(this._serviceUrl + playerId, player);
}