something

This commit is contained in:
2025-10-01 01:56:05 +03:00
parent ece9cedb37
commit 40342a0e14
112 changed files with 5468 additions and 5468 deletions

View File

@@ -1,41 +1,41 @@
using LctMonolith.Application.Options;
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Services;
using LctMonolith.Services.Contracts;
using LctMonolith.Services.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace LctMonolith.Application.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
// Unit of Work
services.AddScoped<IUnitOfWork, UnitOfWork>();
// Core domain / gamification services
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IStoreService, StoreService>();
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<IAnalyticsService, AnalyticsService>();
services.AddScoped<IPlayerService, PlayerService>();
services.AddScoped<IRankService, RankService>();
services.AddScoped<ISkillService, SkillService>();
services.AddScoped<IMissionCategoryService, MissionCategoryService>();
services.AddScoped<IMissionService, MissionService>();
services.AddScoped<IRewardService, RewardService>();
services.AddScoped<IRuleValidationService, RuleValidationService>();
services.AddScoped<IProgressTrackingService, ProgressTrackingService>();
services.AddScoped<IDialogueService, DialogueService>();
services.AddScoped<IInventoryService, InventoryService>();
services.Configure<S3StorageOptions>(configuration.GetSection("S3"));
services.AddSingleton<IFileStorageService, S3FileStorageService>();
services.AddScoped<IProfileService, ProfileService>();
return services;
}
}
using LctMonolith.Application.Options;
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Services;
using LctMonolith.Services.Contracts;
using LctMonolith.Services.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace LctMonolith.Application.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
// Unit of Work
services.AddScoped<IUnitOfWork, UnitOfWork>();
// Core domain / gamification services
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IStoreService, StoreService>();
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<IAnalyticsService, AnalyticsService>();
services.AddScoped<IPlayerService, PlayerService>();
services.AddScoped<IRankService, RankService>();
services.AddScoped<ISkillService, SkillService>();
services.AddScoped<IMissionCategoryService, MissionCategoryService>();
services.AddScoped<IMissionService, MissionService>();
services.AddScoped<IRewardService, RewardService>();
services.AddScoped<IRuleValidationService, RuleValidationService>();
services.AddScoped<IProgressTrackingService, ProgressTrackingService>();
services.AddScoped<IDialogueService, DialogueService>();
services.AddScoped<IInventoryService, InventoryService>();
services.Configure<S3StorageOptions>(configuration.GetSection("S3"));
services.AddSingleton<IFileStorageService, S3FileStorageService>();
services.AddScoped<IProfileService, ProfileService>();
return services;
}
}

View File

@@ -1,46 +1,46 @@
using System.Net;
using System.Text.Json;
using Serilog;
namespace LctMonolith.Application.Middleware;
/// <summary>
/// Global error handling middleware capturing unhandled exceptions and converting to standardized JSON response.
/// </summary>
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(RequestDelegate next) => _next = next;
public async Task Invoke(HttpContext ctx)
{
try
{
await _next(ctx);
}
catch (OperationCanceledException)
{
// Client aborted request (non-standard 499 code used by some proxies)
if (!ctx.Response.HasStarted)
{
ctx.Response.StatusCode = 499; // Client Closed Request (custom)
}
}
catch (Exception ex)
{
Log.Error(ex, "Unhandled exception");
if (ctx.Response.HasStarted) throw;
ctx.Response.ContentType = "application/json";
ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var payload = new { error = new { message = ex.Message, traceId = ctx.TraceIdentifier } };
await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload));
}
}
}
public static class ErrorHandlingMiddlewareExtensions
{
/// <summary>Adds global error handling middleware.</summary>
public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder app) => app.UseMiddleware<ErrorHandlingMiddleware>();
}
using System.Net;
using System.Text.Json;
using Serilog;
namespace LctMonolith.Application.Middleware;
/// <summary>
/// Global error handling middleware capturing unhandled exceptions and converting to standardized JSON response.
/// </summary>
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(RequestDelegate next) => _next = next;
public async Task Invoke(HttpContext ctx)
{
try
{
await _next(ctx);
}
catch (OperationCanceledException)
{
// Client aborted request (non-standard 499 code used by some proxies)
if (!ctx.Response.HasStarted)
{
ctx.Response.StatusCode = 499; // Client Closed Request (custom)
}
}
catch (Exception ex)
{
Log.Error(ex, "Unhandled exception");
if (ctx.Response.HasStarted) throw;
ctx.Response.ContentType = "application/json";
ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var payload = new { error = new { message = ex.Message, traceId = ctx.TraceIdentifier } };
await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload));
}
}
}
public static class ErrorHandlingMiddlewareExtensions
{
/// <summary>Adds global error handling middleware.</summary>
public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder app) => app.UseMiddleware<ErrorHandlingMiddleware>();
}

View File

@@ -1,14 +1,14 @@
namespace LctMonolith.Application.Options;
/// <summary>
/// JWT issuing configuration loaded from appsettings (section Jwt).
/// </summary>
public class JwtOptions
{
public string Key { get; set; } = string.Empty;
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int AccessTokenMinutes { get; set; } = 60;
public int RefreshTokenDays { get; set; } = 7;
}
namespace LctMonolith.Application.Options;
/// <summary>
/// JWT issuing configuration loaded from appsettings (section Jwt).
/// </summary>
public class JwtOptions
{
public string Key { get; set; } = string.Empty;
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int AccessTokenMinutes { get; set; } = 60;
public int RefreshTokenDays { get; set; } = 7;
}

View File

@@ -1,12 +1,12 @@
namespace LctMonolith.Application.Options;
public class S3StorageOptions
{
public string Endpoint { get; set; } = string.Empty;
public bool UseSsl { get; set; } = true;
public string AccessKey { get; set; } = string.Empty;
public string SecretKey { get; set; } = string.Empty;
public string Bucket { get; set; } = "avatars";
public string? PublicBaseUrl { get; set; } // optional CDN / reverse proxy base
public int PresignExpirationMinutes { get; set; } = 60;
}
namespace LctMonolith.Application.Options;
public class S3StorageOptions
{
public string Endpoint { get; set; } = string.Empty;
public bool UseSsl { get; set; } = true;
public string AccessKey { get; set; } = string.Empty;
public string SecretKey { get; set; } = string.Empty;
public string Bucket { get; set; } = "avatars";
public string? PublicBaseUrl { get; set; } // optional CDN / reverse proxy base
public int PresignExpirationMinutes { get; set; } = 60;
}

View File

@@ -1,29 +1,29 @@
using LctMonolith.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
/// <summary>
/// Basic analytics endpoints.
/// </summary>
[ApiController]
[Route("api/analytics")]
[Authorize]
public class AnalyticsController : ControllerBase
{
private readonly IAnalyticsService _analytics;
public AnalyticsController(IAnalyticsService analytics)
{
_analytics = analytics;
}
/// <summary>Get aggregate system summary metrics.</summary>
[HttpGet("summary")]
public async Task<IActionResult> GetSummary(CancellationToken ct)
{
var summary = await _analytics.GetSummaryAsync(ct);
return Ok(summary);
}
}
using LctMonolith.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
/// <summary>
/// Basic analytics endpoints.
/// </summary>
[ApiController]
[Route("api/analytics")]
[Authorize]
public class AnalyticsController : ControllerBase
{
private readonly IAnalyticsService _analytics;
public AnalyticsController(IAnalyticsService analytics)
{
_analytics = analytics;
}
/// <summary>Get aggregate system summary metrics.</summary>
[HttpGet("summary")]
public async Task<IActionResult> GetSummary(CancellationToken ct)
{
var summary = await _analytics.GetSummaryAsync(ct);
return Ok(summary);
}
}

View File

@@ -1,86 +1,86 @@
using System.Security.Claims;
using LctMonolith.Models.Database;
using LctMonolith.Services;
using LctMonolith.Services.Contracts;
using LctMonolith.Services.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.AspNetCore.Mvc;
using RefreshRequest = LctMonolith.Services.Models.RefreshRequest;
namespace LctMonolith.Controllers;
/// <summary>
/// Authentication endpoints (mocked local identity + JWT issuing).
/// </summary>
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager;
private readonly ITokenService _tokenService;
public AuthController(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager, ITokenService tokenService)
{
_userManager = userManager;
_signInManager = signInManager;
_tokenService = tokenService;
}
/// <summary>Registers a new user (simplified).</summary>
[HttpPost("register")]
[AllowAnonymous]
public async Task<ActionResult<TokenPair>> Register(AuthRequest req, CancellationToken ct)
{
var existing = await _userManager.FindByEmailAsync(req.Email);
if (existing != null) return Conflict("Email already registered");
var user = new AppUser { UserName = req.Email, Email = req.Email, FirstName = req.FirstName, LastName = req.LastName };
var result = await _userManager.CreateAsync(user, req.Password);
if (!result.Succeeded) return BadRequest(result.Errors);
var tokens = await _tokenService.IssueAsync(user, ct);
return Ok(tokens);
}
/// <summary>Login with email + password.</summary>
[HttpPost("login")]
[AllowAnonymous]
public async Task<ActionResult<TokenPair>> Login(AuthRequest req, CancellationToken ct)
{
var user = await _userManager.FindByEmailAsync(req.Email);
if (user == null) return Unauthorized();
var passOk = await _signInManager.CheckPasswordSignInAsync(user, req.Password, lockoutOnFailure: false);
if (!passOk.Succeeded) return Unauthorized();
var tokens = await _tokenService.IssueAsync(user, ct);
return Ok(tokens);
}
/// <summary>Refresh access token by refresh token.</summary>
[HttpPost("refresh")]
[AllowAnonymous]
public async Task<ActionResult<TokenPair>> Refresh(RefreshRequest req, CancellationToken ct)
{
var pair = await _tokenService.RefreshAsync(req.RefreshToken, ct);
return Ok(pair);
}
/// <summary>Revoke refresh token (logout).</summary>
[HttpPost("revoke")]
[Authorize]
public async Task<IActionResult> Revoke(RevokeRequest req, CancellationToken ct)
{
await _tokenService.RevokeAsync(req.RefreshToken, ct);
return NoContent();
}
/// <summary>Returns current user id (debug).</summary>
[HttpGet("me")]
[Authorize]
public ActionResult<object> Me()
{
var id = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.Name);
return Ok(new { userId = id });
}
}
using System.Security.Claims;
using LctMonolith.Models.Database;
using LctMonolith.Services;
using LctMonolith.Services.Contracts;
using LctMonolith.Services.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.AspNetCore.Mvc;
using RefreshRequest = LctMonolith.Services.Models.RefreshRequest;
namespace LctMonolith.Controllers;
/// <summary>
/// Authentication endpoints (mocked local identity + JWT issuing).
/// </summary>
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager;
private readonly ITokenService _tokenService;
public AuthController(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager, ITokenService tokenService)
{
_userManager = userManager;
_signInManager = signInManager;
_tokenService = tokenService;
}
/// <summary>Registers a new user (simplified).</summary>
[HttpPost("register")]
[AllowAnonymous]
public async Task<ActionResult<TokenPair>> Register(AuthRequest req, CancellationToken ct)
{
var existing = await _userManager.FindByEmailAsync(req.Email);
if (existing != null) return Conflict("Email already registered");
var user = new AppUser { UserName = req.Email, Email = req.Email, FirstName = req.FirstName, LastName = req.LastName };
var result = await _userManager.CreateAsync(user, req.Password);
if (!result.Succeeded) return BadRequest(result.Errors);
var tokens = await _tokenService.IssueAsync(user, ct);
return Ok(tokens);
}
/// <summary>Login with email + password.</summary>
[HttpPost("login")]
[AllowAnonymous]
public async Task<ActionResult<TokenPair>> Login(AuthRequest req, CancellationToken ct)
{
var user = await _userManager.FindByEmailAsync(req.Email);
if (user == null) return Unauthorized();
var passOk = await _signInManager.CheckPasswordSignInAsync(user, req.Password, lockoutOnFailure: false);
if (!passOk.Succeeded) return Unauthorized();
var tokens = await _tokenService.IssueAsync(user, ct);
return Ok(tokens);
}
/// <summary>Refresh access token by refresh token.</summary>
[HttpPost("refresh")]
[AllowAnonymous]
public async Task<ActionResult<TokenPair>> Refresh(RefreshRequest req, CancellationToken ct)
{
var pair = await _tokenService.RefreshAsync(req.RefreshToken, ct);
return Ok(pair);
}
/// <summary>Revoke refresh token (logout).</summary>
[HttpPost("revoke")]
[Authorize]
public async Task<IActionResult> Revoke(RevokeRequest req, CancellationToken ct)
{
await _tokenService.RevokeAsync(req.RefreshToken, ct);
return NoContent();
}
/// <summary>Returns current user id (debug).</summary>
[HttpGet("me")]
[Authorize]
public ActionResult<object> Me()
{
var id = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.Name);
return Ok(new { userId = id });
}
}

View File

@@ -1,75 +1,75 @@
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/dialogue")]
[Authorize]
public class DialogueController : ControllerBase
{
private readonly IDialogueService _dialogueService;
public DialogueController(IDialogueService dialogueService)
{
_dialogueService = dialogueService;
}
[HttpGet("mission/{missionId:guid}")]
public async Task<IActionResult> GetByMission(Guid missionId)
{
var d = await _dialogueService.GetDialogueByMissionIdAsync(missionId);
return d == null ? NotFound() : Ok(d);
}
[HttpGet("message/{messageId:guid}")]
public async Task<IActionResult> GetMessage(Guid messageId)
{
var m = await _dialogueService.GetDialogueMessageByIdAsync(messageId);
return m == null ? NotFound() : Ok(m);
}
[HttpGet("message/{messageId:guid}/options")]
public async Task<IActionResult> GetOptions(Guid messageId)
{
var opts = await _dialogueService.GetResponseOptionsAsync(messageId);
return Ok(opts);
}
public record DialogueResponseRequest(Guid ResponseOptionId, Guid PlayerId);
[HttpPost("message/{messageId:guid}/respond")]
public async Task<IActionResult> Respond(Guid messageId, DialogueResponseRequest req)
{
var next = await _dialogueService.ProcessDialogueResponseAsync(messageId, req.ResponseOptionId, req.PlayerId);
if (next == null) return Ok(new { end = true });
return Ok(next);
}
public class CreateDialogueRequest
{
public Guid MissionId { get; set; }
public Guid InitialDialogueMessageId { get; set; }
public Guid InterimDialogueMessageId { get; set; }
public Guid EndDialogueMessageId { get; set; }
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateDialogueRequest dto)
{
var d = new Dialogue
{
Id = Guid.NewGuid(),
MissionId = dto.MissionId,
InitialDialogueMessageId = dto.InitialDialogueMessageId,
InterimDialogueMessageId = dto.InterimDialogueMessageId,
EndDialogueMessageId = dto.EndDialogueMessageId,
Mission = null! // EF will populate if included
};
d = await _dialogueService.CreateDialogueAsync(d);
return CreatedAtAction(nameof(GetByMission), new { missionId = d.MissionId }, d);
}
}
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/dialogue")]
[Authorize]
public class DialogueController : ControllerBase
{
private readonly IDialogueService _dialogueService;
public DialogueController(IDialogueService dialogueService)
{
_dialogueService = dialogueService;
}
[HttpGet("mission/{missionId:guid}")]
public async Task<IActionResult> GetByMission(Guid missionId)
{
var d = await _dialogueService.GetDialogueByMissionIdAsync(missionId);
return d == null ? NotFound() : Ok(d);
}
[HttpGet("message/{messageId:guid}")]
public async Task<IActionResult> GetMessage(Guid messageId)
{
var m = await _dialogueService.GetDialogueMessageByIdAsync(messageId);
return m == null ? NotFound() : Ok(m);
}
[HttpGet("message/{messageId:guid}/options")]
public async Task<IActionResult> GetOptions(Guid messageId)
{
var opts = await _dialogueService.GetResponseOptionsAsync(messageId);
return Ok(opts);
}
public record DialogueResponseRequest(Guid ResponseOptionId, Guid PlayerId);
[HttpPost("message/{messageId:guid}/respond")]
public async Task<IActionResult> Respond(Guid messageId, DialogueResponseRequest req)
{
var next = await _dialogueService.ProcessDialogueResponseAsync(messageId, req.ResponseOptionId, req.PlayerId);
if (next == null) return Ok(new { end = true });
return Ok(next);
}
public class CreateDialogueRequest
{
public Guid MissionId { get; set; }
public Guid InitialDialogueMessageId { get; set; }
public Guid InterimDialogueMessageId { get; set; }
public Guid EndDialogueMessageId { get; set; }
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateDialogueRequest dto)
{
var d = new Dialogue
{
Id = Guid.NewGuid(),
MissionId = dto.MissionId,
InitialDialogueMessageId = dto.InitialDialogueMessageId,
InterimDialogueMessageId = dto.InterimDialogueMessageId,
EndDialogueMessageId = dto.EndDialogueMessageId,
Mission = null! // EF will populate if included
};
d = await _dialogueService.CreateDialogueAsync(d);
return CreatedAtAction(nameof(GetByMission), new { missionId = d.MissionId }, d);
}
}

View File

@@ -1,34 +1,34 @@
using System.Security.Claims;
using LctMonolith.Services.Contracts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/inventory")]
[Authorize]
public class InventoryController : ControllerBase
{
private readonly IInventoryService _inventoryService;
public InventoryController(IInventoryService inventoryService) => _inventoryService = inventoryService;
private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
/// <summary>Get inventory for current authenticated user.</summary>
[HttpGet]
public async Task<IActionResult> GetMine(CancellationToken ct)
{
var items = await _inventoryService.GetStoreInventoryAsync(CurrentUserId(), ct);
return Ok(items.Select(i => new { i.StoreItemId, i.Quantity, i.AcquiredAt }));
}
/// <summary>Admin: get inventory for specific user.</summary>
[HttpGet("user/{userId:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> GetByUser(Guid userId, CancellationToken ct)
{
var items = await _inventoryService.GetStoreInventoryAsync(userId, ct);
return Ok(items);
}
}
using System.Security.Claims;
using LctMonolith.Services.Contracts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/inventory")]
[Authorize]
public class InventoryController : ControllerBase
{
private readonly IInventoryService _inventoryService;
public InventoryController(IInventoryService inventoryService) => _inventoryService = inventoryService;
private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
/// <summary>Get inventory for current authenticated user.</summary>
[HttpGet]
public async Task<IActionResult> GetMine(CancellationToken ct)
{
var items = await _inventoryService.GetStoreInventoryAsync(CurrentUserId(), ct);
return Ok(items.Select(i => new { i.StoreItemId, i.Quantity, i.AcquiredAt }));
}
/// <summary>Admin: get inventory for specific user.</summary>
[HttpGet("user/{userId:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> GetByUser(Guid userId, CancellationToken ct)
{
var items = await _inventoryService.GetStoreInventoryAsync(userId, ct);
return Ok(items);
}
}

View File

@@ -1,58 +1,58 @@
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/mission-categories")]
[Authorize]
public class MissionCategoriesController : ControllerBase
{
private readonly IMissionCategoryService _service;
public MissionCategoriesController(IMissionCategoryService service) => _service = service;
[HttpGet]
public async Task<IActionResult> GetAll()
{
var list = await _service.GetAllCategoriesAsync();
return Ok(list);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id)
{
var c = await _service.GetCategoryByIdAsync(id);
return c == null ? NotFound() : Ok(c);
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateMissionCategoryDto dto)
{
var c = await _service.CreateCategoryAsync(new MissionCategory { Title = dto.Title });
return CreatedAtAction(nameof(Get), new { id = c.Id }, c);
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Update(Guid id, CreateMissionCategoryDto dto)
{
var c = await _service.GetCategoryByIdAsync(id);
if (c == null) return NotFound();
c.Title = dto.Title;
await _service.UpdateCategoryAsync(c);
return Ok(c);
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(Guid id)
{
var ok = await _service.DeleteCategoryAsync(id);
return ok ? NoContent() : NotFound();
}
}
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/mission-categories")]
[Authorize]
public class MissionCategoriesController : ControllerBase
{
private readonly IMissionCategoryService _service;
public MissionCategoriesController(IMissionCategoryService service) => _service = service;
[HttpGet]
public async Task<IActionResult> GetAll()
{
var list = await _service.GetAllCategoriesAsync();
return Ok(list);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id)
{
var c = await _service.GetCategoryByIdAsync(id);
return c == null ? NotFound() : Ok(c);
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateMissionCategoryDto dto)
{
var c = await _service.CreateCategoryAsync(new MissionCategory { Title = dto.Title });
return CreatedAtAction(nameof(Get), new { id = c.Id }, c);
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Update(Guid id, CreateMissionCategoryDto dto)
{
var c = await _service.GetCategoryByIdAsync(id);
if (c == null) return NotFound();
c.Title = dto.Title;
await _service.UpdateCategoryAsync(c);
return Ok(c);
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(Guid id)
{
var ok = await _service.DeleteCategoryAsync(id);
return ok ? NoContent() : NotFound();
}
}

View File

@@ -1,112 +1,112 @@
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/missions")]
[Authorize]
public class MissionsController : ControllerBase
{
private readonly IMissionService _missions;
private readonly IRuleValidationService _rules;
public MissionsController(IMissionService missions, IRuleValidationService rules)
{
_missions = missions;
_rules = rules;
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id)
{
var m = await _missions.GetMissionByIdAsync(id);
return m == null ? NotFound() : Ok(m);
}
[HttpGet("category/{categoryId:guid}")]
public async Task<IActionResult> ByCategory(Guid categoryId)
{
var list = await _missions.GetMissionsByCategoryAsync(categoryId);
return Ok(list);
}
[HttpGet("player/{playerId:guid}/available")]
public async Task<IActionResult> Available(Guid playerId)
{
var list = await _missions.GetAvailableMissionsForPlayerAsync(playerId);
return Ok(list);
}
[HttpGet("{id:guid}/rank-rules")]
public async Task<IActionResult> RankRules(Guid id)
{
var rules = await _rules.GetApplicableRankRulesAsync(id);
return Ok(rules);
}
public class CreateMissionRequest
{
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public Guid MissionCategoryId { get; set; }
public Guid? ParentMissionId { get; set; }
public int ExpReward { get; set; }
public int ManaReward { get; set; }
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateMissionRequest dto)
{
var mission = new Mission
{
Title = dto.Title,
Description = dto.Description ?? string.Empty,
MissionCategoryId = dto.MissionCategoryId,
ParentMissionId = dto.ParentMissionId,
ExpReward = dto.ExpReward,
ManaReward = dto.ManaReward
};
mission = await _missions.CreateMissionAsync(mission);
return CreatedAtAction(nameof(Get), new { id = mission.Id }, mission);
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Update(Guid id, CreateMissionRequest dto)
{
var existing = await _missions.GetMissionByIdAsync(id);
if (existing == null) return NotFound();
existing.Title = dto.Title;
existing.Description = dto.Description ?? string.Empty;
existing.MissionCategoryId = dto.MissionCategoryId;
existing.ParentMissionId = dto.ParentMissionId;
existing.ExpReward = dto.ExpReward;
existing.ManaReward = dto.ManaReward;
await _missions.UpdateMissionAsync(existing);
return Ok(existing);
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(Guid id)
{
var ok = await _missions.DeleteMissionAsync(id);
return ok ? NoContent() : NotFound();
}
public record CompleteMissionRequest(Guid PlayerId, object? Proof);
[HttpPost("{missionId:guid}/complete")]
public async Task<IActionResult> Complete(Guid missionId, CompleteMissionRequest r)
{
var result = await _missions.CompleteMissionAsync(missionId, r.PlayerId, r.Proof);
if (!result.Success) return BadRequest(result);
return Ok(result);
}
}
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/missions")]
[Authorize]
public class MissionsController : ControllerBase
{
private readonly IMissionService _missions;
private readonly IRuleValidationService _rules;
public MissionsController(IMissionService missions, IRuleValidationService rules)
{
_missions = missions;
_rules = rules;
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id)
{
var m = await _missions.GetMissionByIdAsync(id);
return m == null ? NotFound() : Ok(m);
}
[HttpGet("category/{categoryId:guid}")]
public async Task<IActionResult> ByCategory(Guid categoryId)
{
var list = await _missions.GetMissionsByCategoryAsync(categoryId);
return Ok(list);
}
[HttpGet("player/{playerId:guid}/available")]
public async Task<IActionResult> Available(Guid playerId)
{
var list = await _missions.GetAvailableMissionsForPlayerAsync(playerId);
return Ok(list);
}
[HttpGet("{id:guid}/rank-rules")]
public async Task<IActionResult> RankRules(Guid id)
{
var rules = await _rules.GetApplicableRankRulesAsync(id);
return Ok(rules);
}
public class CreateMissionRequest
{
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public Guid MissionCategoryId { get; set; }
public Guid? ParentMissionId { get; set; }
public int ExpReward { get; set; }
public int ManaReward { get; set; }
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateMissionRequest dto)
{
var mission = new Mission
{
Title = dto.Title,
Description = dto.Description ?? string.Empty,
MissionCategoryId = dto.MissionCategoryId,
ParentMissionId = dto.ParentMissionId,
ExpReward = dto.ExpReward,
ManaReward = dto.ManaReward
};
mission = await _missions.CreateMissionAsync(mission);
return CreatedAtAction(nameof(Get), new { id = mission.Id }, mission);
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Update(Guid id, CreateMissionRequest dto)
{
var existing = await _missions.GetMissionByIdAsync(id);
if (existing == null) return NotFound();
existing.Title = dto.Title;
existing.Description = dto.Description ?? string.Empty;
existing.MissionCategoryId = dto.MissionCategoryId;
existing.ParentMissionId = dto.ParentMissionId;
existing.ExpReward = dto.ExpReward;
existing.ManaReward = dto.ManaReward;
await _missions.UpdateMissionAsync(existing);
return Ok(existing);
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(Guid id)
{
var ok = await _missions.DeleteMissionAsync(id);
return ok ? NoContent() : NotFound();
}
public record CompleteMissionRequest(Guid PlayerId, object? Proof);
[HttpPost("{missionId:guid}/complete")]
public async Task<IActionResult> Complete(Guid missionId, CompleteMissionRequest r)
{
var result = await _missions.CompleteMissionAsync(missionId, r.PlayerId, r.Proof);
if (!result.Success) return BadRequest(result);
return Ok(result);
}
}

View File

@@ -1,58 +1,58 @@
using System.Security.Claims;
using LctMonolith.Services;
using LctMonolith.Services.Contracts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
/// <summary>
/// In-app user notifications API.
/// </summary>
[ApiController]
[Route("api/notifications")]
[Authorize]
public class NotificationsController : ControllerBase
{
private readonly INotificationService _notifications;
public NotificationsController(INotificationService notifications)
{
_notifications = notifications;
}
private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
/// <summary>Get up to 100 unread notifications.</summary>
[HttpGet("unread")]
public async Task<IActionResult> GetUnread(CancellationToken ct)
{
var list = await _notifications.GetUnreadAsync(GetUserId(), ct);
return Ok(list);
}
/// <summary>Get recent notifications (paged by take).</summary>
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] int take = 100, CancellationToken ct = default)
{
var list = await _notifications.GetAllAsync(GetUserId(), take, ct);
return Ok(list);
}
/// <summary>Mark a notification as read.</summary>
[HttpPost("mark/{id:guid}")]
public async Task<IActionResult> MarkRead(Guid id, CancellationToken ct)
{
await _notifications.MarkReadAsync(GetUserId(), id, ct);
return NoContent();
}
/// <summary>Mark all notifications as read.</summary>
[HttpPost("mark-all")]
public async Task<IActionResult> MarkAll(CancellationToken ct)
{
var cnt = await _notifications.MarkAllReadAsync(GetUserId(), ct);
return Ok(new { updated = cnt });
}
}
using System.Security.Claims;
using LctMonolith.Services;
using LctMonolith.Services.Contracts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
/// <summary>
/// In-app user notifications API.
/// </summary>
[ApiController]
[Route("api/notifications")]
[Authorize]
public class NotificationsController : ControllerBase
{
private readonly INotificationService _notifications;
public NotificationsController(INotificationService notifications)
{
_notifications = notifications;
}
private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
/// <summary>Get up to 100 unread notifications.</summary>
[HttpGet("unread")]
public async Task<IActionResult> GetUnread(CancellationToken ct)
{
var list = await _notifications.GetUnreadAsync(GetUserId(), ct);
return Ok(list);
}
/// <summary>Get recent notifications (paged by take).</summary>
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] int take = 100, CancellationToken ct = default)
{
var list = await _notifications.GetAllAsync(GetUserId(), take, ct);
return Ok(list);
}
/// <summary>Mark a notification as read.</summary>
[HttpPost("mark/{id:guid}")]
public async Task<IActionResult> MarkRead(Guid id, CancellationToken ct)
{
await _notifications.MarkReadAsync(GetUserId(), id, ct);
return NoContent();
}
/// <summary>Mark all notifications as read.</summary>
[HttpPost("mark-all")]
public async Task<IActionResult> MarkAll(CancellationToken ct)
{
var cnt = await _notifications.MarkAllReadAsync(GetUserId(), ct);
return Ok(new { updated = cnt });
}
}

View File

@@ -1,78 +1,78 @@
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/players")]
[Authorize]
public class PlayersController : ControllerBase
{
private readonly IPlayerService _playerService;
private readonly IProgressTrackingService _progressService;
public PlayersController(IPlayerService playerService, IProgressTrackingService progressService)
{
_playerService = playerService;
_progressService = progressService;
}
[HttpGet("{playerId:guid}")]
public async Task<IActionResult> GetPlayer(Guid playerId)
{
var player = await _playerService.GetPlayerWithProgressAsync(playerId);
return Ok(player);
}
[HttpGet("user/{userId:guid}")]
public async Task<IActionResult> GetByUser(Guid userId)
{
var p = await _playerService.GetPlayerByUserIdAsync(userId.ToString());
if (p == null) return NotFound();
return Ok(p);
}
public class CreatePlayerRequest { public Guid UserId { get; set; } public string Username { get; set; } = string.Empty; }
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreatePlayerRequest req)
{
var p = await _playerService.CreatePlayerAsync(req.UserId.ToString(), req.Username);
return CreatedAtAction(nameof(GetPlayer), new { playerId = p.Id }, p);
}
public record AdjustValueRequest(int Value);
[HttpPost("{playerId:guid}/experience")]
[Authorize(Roles = "Admin")] // manual adjust
public async Task<IActionResult> AddExperience(Guid playerId, AdjustValueRequest r)
{
var p = await _playerService.AddPlayerExperienceAsync(playerId, r.Value);
return Ok(new { p.Id, p.Experience });
}
[HttpPost("{playerId:guid}/mana")]
[Authorize(Roles = "Admin")] // manual adjust
public async Task<IActionResult> AddMana(Guid playerId, AdjustValueRequest r)
{
var p = await _playerService.AddPlayerManaAsync(playerId, r.Value);
return Ok(new { p.Id, p.Mana });
}
[HttpGet("top")]
public async Task<IActionResult> GetTop([FromQuery] int count = 10)
{
var list = await _playerService.GetTopPlayersAsync(count, TimeSpan.FromDays(30));
return Ok(list);
}
[HttpGet("{playerId:guid}/progress")]
public async Task<IActionResult> GetProgress(Guid playerId)
{
var prog = await _progressService.GetPlayerOverallProgressAsync(playerId);
return Ok(prog);
}
}
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/players")]
[Authorize]
public class PlayersController : ControllerBase
{
private readonly IPlayerService _playerService;
private readonly IProgressTrackingService _progressService;
public PlayersController(IPlayerService playerService, IProgressTrackingService progressService)
{
_playerService = playerService;
_progressService = progressService;
}
[HttpGet("{playerId:guid}")]
public async Task<IActionResult> GetPlayer(Guid playerId)
{
var player = await _playerService.GetPlayerWithProgressAsync(playerId);
return Ok(player);
}
[HttpGet("user/{userId:guid}")]
public async Task<IActionResult> GetByUser(Guid userId)
{
var p = await _playerService.GetPlayerByUserIdAsync(userId.ToString());
if (p == null) return NotFound();
return Ok(p);
}
public class CreatePlayerRequest { public Guid UserId { get; set; } public string Username { get; set; } = string.Empty; }
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreatePlayerRequest req)
{
var p = await _playerService.CreatePlayerAsync(req.UserId.ToString(), req.Username);
return CreatedAtAction(nameof(GetPlayer), new { playerId = p.Id }, p);
}
public record AdjustValueRequest(int Value);
[HttpPost("{playerId:guid}/experience")]
[Authorize(Roles = "Admin")] // manual adjust
public async Task<IActionResult> AddExperience(Guid playerId, AdjustValueRequest r)
{
var p = await _playerService.AddPlayerExperienceAsync(playerId, r.Value);
return Ok(new { p.Id, p.Experience });
}
[HttpPost("{playerId:guid}/mana")]
[Authorize(Roles = "Admin")] // manual adjust
public async Task<IActionResult> AddMana(Guid playerId, AdjustValueRequest r)
{
var p = await _playerService.AddPlayerManaAsync(playerId, r.Value);
return Ok(new { p.Id, p.Mana });
}
[HttpGet("top")]
public async Task<IActionResult> GetTop([FromQuery] int count = 10)
{
var list = await _playerService.GetTopPlayersAsync(count, TimeSpan.FromDays(30));
return Ok(list);
}
[HttpGet("{playerId:guid}/progress")]
public async Task<IActionResult> GetProgress(Guid playerId)
{
var prog = await _progressService.GetPlayerOverallProgressAsync(playerId);
return Ok(prog);
}
}

View File

@@ -1,67 +1,67 @@
using System.Security.Claims;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/profile")]
[Authorize]
public class ProfileController : ControllerBase
{
private readonly IProfileService _profiles;
public ProfileController(IProfileService profiles) => _profiles = profiles;
private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
public class UpdateProfileDto
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateOnly? BirthDate { get; set; }
public string? About { get; set; }
public string? Location { get; set; }
}
[HttpGet("me")]
public async Task<IActionResult> GetMe(CancellationToken ct)
{
var p = await _profiles.GetByUserIdAsync(CurrentUserId(), ct);
if (p == null) return NotFound();
return Ok(p);
}
[HttpGet("{userId:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> GetByUser(Guid userId, CancellationToken ct)
{
var p = await _profiles.GetByUserIdAsync(userId, ct);
return p == null ? NotFound() : Ok(p);
}
[HttpPut]
public async Task<IActionResult> Upsert(UpdateProfileDto dto, CancellationToken ct)
{
var p = await _profiles.UpsertAsync(CurrentUserId(), dto.FirstName, dto.LastName, dto.BirthDate, dto.About, dto.Location, ct);
return Ok(p);
}
[HttpPost("avatar")]
[RequestSizeLimit(7_000_000)] // ~7MB
public async Task<IActionResult> UploadAvatar(IFormFile file, CancellationToken ct)
{
if (file == null || file.Length == 0) return BadRequest("File required");
await using var stream = file.OpenReadStream();
var p = await _profiles.UpdateAvatarAsync(CurrentUserId(), stream, file.ContentType, file.FileName, ct);
return Ok(new { p.AvatarUrl });
}
[HttpDelete("avatar")]
public async Task<IActionResult> DeleteAvatar(CancellationToken ct)
{
var ok = await _profiles.DeleteAvatarAsync(CurrentUserId(), ct);
return ok ? NoContent() : NotFound();
}
}
using System.Security.Claims;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/profile")]
[Authorize]
public class ProfileController : ControllerBase
{
private readonly IProfileService _profiles;
public ProfileController(IProfileService profiles) => _profiles = profiles;
private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
public class UpdateProfileDto
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateOnly? BirthDate { get; set; }
public string? About { get; set; }
public string? Location { get; set; }
}
[HttpGet("me")]
public async Task<IActionResult> GetMe(CancellationToken ct)
{
var p = await _profiles.GetByUserIdAsync(CurrentUserId(), ct);
if (p == null) return NotFound();
return Ok(p);
}
[HttpGet("{userId:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> GetByUser(Guid userId, CancellationToken ct)
{
var p = await _profiles.GetByUserIdAsync(userId, ct);
return p == null ? NotFound() : Ok(p);
}
[HttpPut]
public async Task<IActionResult> Upsert(UpdateProfileDto dto, CancellationToken ct)
{
var p = await _profiles.UpsertAsync(CurrentUserId(), dto.FirstName, dto.LastName, dto.BirthDate, dto.About, dto.Location, ct);
return Ok(p);
}
[HttpPost("avatar")]
[RequestSizeLimit(7_000_000)] // ~7MB
public async Task<IActionResult> UploadAvatar(IFormFile file, CancellationToken ct)
{
if (file == null || file.Length == 0) return BadRequest("File required");
await using var stream = file.OpenReadStream();
var p = await _profiles.UpdateAvatarAsync(CurrentUserId(), stream, file.ContentType, file.FileName, ct);
return Ok(new { p.AvatarUrl });
}
[HttpDelete("avatar")]
public async Task<IActionResult> DeleteAvatar(CancellationToken ct)
{
var ok = await _profiles.DeleteAvatarAsync(CurrentUserId(), ct);
return ok ? NoContent() : NotFound();
}
}

View File

@@ -1,72 +1,72 @@
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/ranks")]
[Authorize]
public class RanksController : ControllerBase
{
private readonly IRankService _rankService;
private readonly IRuleValidationService _ruleValidation;
public RanksController(IRankService rankService, IRuleValidationService ruleValidation)
{
_rankService = rankService;
_ruleValidation = ruleValidation;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var ranks = await _rankService.GetAllRanksAsync();
return Ok(ranks);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id)
{
var r = await _rankService.GetRankByIdAsync(id);
return r == null ? NotFound() : Ok(r);
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateRankDto dto)
{
var rank = await _rankService.CreateRankAsync(new Rank { Title = dto.Title, ExpNeeded = dto.ExpNeeded });
return CreatedAtAction(nameof(Get), new { id = rank.Id }, rank);
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Update(Guid id, CreateRankDto dto)
{
var r = await _rankService.GetRankByIdAsync(id);
if (r == null) return NotFound();
r.Title = dto.Title;
r.ExpNeeded = dto.ExpNeeded;
await _rankService.UpdateRankAsync(r);
return Ok(r);
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(Guid id)
{
var ok = await _rankService.DeleteRankAsync(id);
return ok ? NoContent() : NotFound();
}
[HttpGet("validate-advance/{playerId:guid}/{targetRankId:guid}")]
public async Task<IActionResult> CanAdvance(Guid playerId, Guid targetRankId)
{
var ok = await _ruleValidation.ValidateRankAdvancementRulesAsync(playerId, targetRankId);
return Ok(new { playerId, targetRankId, canAdvance = ok });
}
}
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/ranks")]
[Authorize]
public class RanksController : ControllerBase
{
private readonly IRankService _rankService;
private readonly IRuleValidationService _ruleValidation;
public RanksController(IRankService rankService, IRuleValidationService ruleValidation)
{
_rankService = rankService;
_ruleValidation = ruleValidation;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var ranks = await _rankService.GetAllRanksAsync();
return Ok(ranks);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id)
{
var r = await _rankService.GetRankByIdAsync(id);
return r == null ? NotFound() : Ok(r);
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateRankDto dto)
{
var rank = await _rankService.CreateRankAsync(new Rank { Title = dto.Title, ExpNeeded = dto.ExpNeeded });
return CreatedAtAction(nameof(Get), new { id = rank.Id }, rank);
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Update(Guid id, CreateRankDto dto)
{
var r = await _rankService.GetRankByIdAsync(id);
if (r == null) return NotFound();
r.Title = dto.Title;
r.ExpNeeded = dto.ExpNeeded;
await _rankService.UpdateRankAsync(r);
return Ok(r);
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(Guid id)
{
var ok = await _rankService.DeleteRankAsync(id);
return ok ? NoContent() : NotFound();
}
[HttpGet("validate-advance/{playerId:guid}/{targetRankId:guid}")]
public async Task<IActionResult> CanAdvance(Guid playerId, Guid targetRankId)
{
var ok = await _ruleValidation.ValidateRankAdvancementRulesAsync(playerId, targetRankId);
return Ok(new { playerId, targetRankId, canAdvance = ok });
}
}

View File

@@ -1,65 +1,65 @@
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/rewards")]
[Authorize]
public class RewardController : ControllerBase
{
private readonly IRewardService _rewardService;
public RewardController(IRewardService rewardService)
{
_rewardService = rewardService;
}
/// <summary>List skill rewards configured for a mission.</summary>
[HttpGet("mission/{missionId:guid}/skills")]
public async Task<IActionResult> GetMissionSkillRewards(Guid missionId)
{
var rewards = await _rewardService.GetMissionSkillRewardsAsync(missionId);
return Ok(rewards.Select(r => new { r.SkillId, r.Value }));
}
/// <summary>List item rewards configured for a mission.</summary>
[HttpGet("mission/{missionId:guid}/items")]
public async Task<IActionResult> GetMissionItemRewards(Guid missionId)
{
var rewards = await _rewardService.GetMissionItemRewardsAsync(missionId);
return Ok(rewards.Select(r => new { r.ItemId }));
}
/// <summary>Check if mission rewards can be claimed by player (missionId used as rewardId).</summary>
[HttpGet("mission/{missionId:guid}/can-claim/{playerId:guid}")]
public async Task<IActionResult> CanClaim(Guid missionId, Guid playerId)
{
var can = await _rewardService.CanClaimRewardAsync(missionId, playerId);
return Ok(new { missionId, playerId, canClaim = can });
}
public record ClaimRewardRequest(Guid PlayerId);
/// <summary>Claim mission rewards if available (idempotent on already claimed).</summary>
[HttpPost("mission/{missionId:guid}/claim")]
public async Task<IActionResult> Claim(Guid missionId, ClaimRewardRequest req)
{
var can = await _rewardService.CanClaimRewardAsync(missionId, req.PlayerId);
if (!can) return Conflict(new { message = "Rewards already claimed or mission not completed" });
await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId);
return Ok(new { missionId, req.PlayerId, status = "claimed" });
}
public record ForceDistributeRequest(Guid PlayerId);
/// <summary>Admin: force distribute rewards regardless of previous state (may duplicate). Use cautiously.</summary>
[HttpPost("mission/{missionId:guid}/force-distribute")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> ForceDistribute(Guid missionId, ForceDistributeRequest req)
{
await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId);
return Ok(new { missionId, req.PlayerId, status = "forced" });
}
}
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/rewards")]
[Authorize]
public class RewardController : ControllerBase
{
private readonly IRewardService _rewardService;
public RewardController(IRewardService rewardService)
{
_rewardService = rewardService;
}
/// <summary>List skill rewards configured for a mission.</summary>
[HttpGet("mission/{missionId:guid}/skills")]
public async Task<IActionResult> GetMissionSkillRewards(Guid missionId)
{
var rewards = await _rewardService.GetMissionSkillRewardsAsync(missionId);
return Ok(rewards.Select(r => new { r.SkillId, r.Value }));
}
/// <summary>List item rewards configured for a mission.</summary>
[HttpGet("mission/{missionId:guid}/items")]
public async Task<IActionResult> GetMissionItemRewards(Guid missionId)
{
var rewards = await _rewardService.GetMissionItemRewardsAsync(missionId);
return Ok(rewards.Select(r => new { r.ItemId }));
}
/// <summary>Check if mission rewards can be claimed by player (missionId used as rewardId).</summary>
[HttpGet("mission/{missionId:guid}/can-claim/{playerId:guid}")]
public async Task<IActionResult> CanClaim(Guid missionId, Guid playerId)
{
var can = await _rewardService.CanClaimRewardAsync(missionId, playerId);
return Ok(new { missionId, playerId, canClaim = can });
}
public record ClaimRewardRequest(Guid PlayerId);
/// <summary>Claim mission rewards if available (idempotent on already claimed).</summary>
[HttpPost("mission/{missionId:guid}/claim")]
public async Task<IActionResult> Claim(Guid missionId, ClaimRewardRequest req)
{
var can = await _rewardService.CanClaimRewardAsync(missionId, req.PlayerId);
if (!can) return Conflict(new { message = "Rewards already claimed or mission not completed" });
await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId);
return Ok(new { missionId, req.PlayerId, status = "claimed" });
}
public record ForceDistributeRequest(Guid PlayerId);
/// <summary>Admin: force distribute rewards regardless of previous state (may duplicate). Use cautiously.</summary>
[HttpPost("mission/{missionId:guid}/force-distribute")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> ForceDistribute(Guid missionId, ForceDistributeRequest req)
{
await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId);
return Ok(new { missionId, req.PlayerId, status = "forced" });
}
}

View File

@@ -1,79 +1,79 @@
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/skills")]
[Authorize]
public class SkillsController : ControllerBase
{
private readonly ISkillService _skillService;
public SkillsController(ISkillService skillService)
{
_skillService = skillService;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var list = await _skillService.GetAllSkillsAsync();
return Ok(list);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id)
{
var s = await _skillService.GetSkillByIdAsync(id);
return s == null ? NotFound() : Ok(s);
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateSkillDto dto)
{
var skill = await _skillService.CreateSkillAsync(new Skill { Title = dto.Title });
return CreatedAtAction(nameof(Get), new { id = skill.Id }, skill);
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Update(Guid id, CreateSkillDto dto)
{
var s = await _skillService.GetSkillByIdAsync(id);
if (s == null) return NotFound();
s.Title = dto.Title;
await _skillService.UpdateSkillAsync(s);
return Ok(s);
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(Guid id)
{
var ok = await _skillService.DeleteSkillAsync(id);
return ok ? NoContent() : NotFound();
}
[HttpGet("player/{playerId:guid}")]
public async Task<IActionResult> PlayerSkills(Guid playerId)
{
var list = await _skillService.GetPlayerSkillsAsync(playerId);
return Ok(list);
}
public record UpdatePlayerSkillRequest(int Level);
[HttpPost("player/{playerId:guid}/{skillId:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdatePlayerSkill(Guid playerId, Guid skillId, UpdatePlayerSkillRequest r)
{
var ps = await _skillService.UpdatePlayerSkillAsync(playerId, skillId, r.Level);
return Ok(new { ps.PlayerId, ps.SkillId, ps.Score });
}
}
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/skills")]
[Authorize]
public class SkillsController : ControllerBase
{
private readonly ISkillService _skillService;
public SkillsController(ISkillService skillService)
{
_skillService = skillService;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var list = await _skillService.GetAllSkillsAsync();
return Ok(list);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id)
{
var s = await _skillService.GetSkillByIdAsync(id);
return s == null ? NotFound() : Ok(s);
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create(CreateSkillDto dto)
{
var skill = await _skillService.CreateSkillAsync(new Skill { Title = dto.Title });
return CreatedAtAction(nameof(Get), new { id = skill.Id }, skill);
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Update(Guid id, CreateSkillDto dto)
{
var s = await _skillService.GetSkillByIdAsync(id);
if (s == null) return NotFound();
s.Title = dto.Title;
await _skillService.UpdateSkillAsync(s);
return Ok(s);
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(Guid id)
{
var ok = await _skillService.DeleteSkillAsync(id);
return ok ? NoContent() : NotFound();
}
[HttpGet("player/{playerId:guid}")]
public async Task<IActionResult> PlayerSkills(Guid playerId)
{
var list = await _skillService.GetPlayerSkillsAsync(playerId);
return Ok(list);
}
public record UpdatePlayerSkillRequest(int Level);
[HttpPost("player/{playerId:guid}/{skillId:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdatePlayerSkill(Guid playerId, Guid skillId, UpdatePlayerSkillRequest r)
{
var ps = await _skillService.UpdatePlayerSkillAsync(playerId, skillId, r.Level);
return Ok(new { ps.PlayerId, ps.SkillId, ps.Score });
}
}

View File

@@ -1,43 +1,43 @@
using System.Security.Claims;
using LctMonolith.Services;
using LctMonolith.Services.Contracts;
using LctMonolith.Services.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
/// <summary>
/// Store endpoints for listing items and purchasing.
/// </summary>
[ApiController]
[Route("api/store")]
[Authorize]
public class StoreController : ControllerBase
{
private readonly IStoreService _storeService;
public StoreController(IStoreService storeService)
{
_storeService = storeService;
}
private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
/// <summary>List active store items.</summary>
[HttpGet("items")]
public async Task<IActionResult> GetItems(CancellationToken ct)
{
var items = await _storeService.GetActiveItemsAsync(ct);
return Ok(items);
}
/// <summary>Purchase an item for the authenticated user.</summary>
[HttpPost("purchase")]
public async Task<IActionResult> Purchase(PurchaseRequest req, CancellationToken ct)
{
var inv = await _storeService.PurchaseAsync(GetUserId(), req.ItemId, req.Quantity, ct);
return Ok(new { inv.StoreItemId, inv.Quantity, inv.AcquiredAt });
}
}
using System.Security.Claims;
using LctMonolith.Services;
using LctMonolith.Services.Contracts;
using LctMonolith.Services.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
/// <summary>
/// Store endpoints for listing items and purchasing.
/// </summary>
[ApiController]
[Route("api/store")]
[Authorize]
public class StoreController : ControllerBase
{
private readonly IStoreService _storeService;
public StoreController(IStoreService storeService)
{
_storeService = storeService;
}
private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
/// <summary>List active store items.</summary>
[HttpGet("items")]
public async Task<IActionResult> GetItems(CancellationToken ct)
{
var items = await _storeService.GetActiveItemsAsync(ct);
return Ok(items);
}
/// <summary>Purchase an item for the authenticated user.</summary>
[HttpPost("purchase")]
public async Task<IActionResult> Purchase(PurchaseRequest req, CancellationToken ct)
{
var inv = await _storeService.PurchaseAsync(GetUserId(), req.ItemId, req.Quantity, ct);
return Ok(new { inv.StoreItemId, inv.Quantity, inv.AcquiredAt });
}
}

View File

@@ -1,224 +1,224 @@
using LctMonolith.Models.Database;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.Data;
/// <summary>
/// Main EF Core database context for gamification module (PostgreSQL provider expected).
/// </summary>
public class AppDbContext : IdentityDbContext<AppUser, IdentityRole<Guid>, Guid>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
// Rank related entities
public DbSet<Rank> Ranks => Set<Rank>();
public DbSet<RankMissionRule> RankMissionRules => Set<RankMissionRule>();
public DbSet<RankSkillRule> RankSkillRules => Set<RankSkillRule>();
// Mission related entities
public DbSet<MissionCategory> MissionCategories => Set<MissionCategory>();
public DbSet<Mission> Missions => Set<Mission>();
public DbSet<PlayerMission> PlayerMissions => Set<PlayerMission>();
public DbSet<MissionSkillReward> MissionSkillRewards => Set<MissionSkillReward>();
public DbSet<MissionItemReward> MissionItemRewards => Set<MissionItemReward>();
public DbSet<MissionRankRule> MissionRankRules => Set<MissionRankRule>();
// Skill related entities
public DbSet<Skill> Skills => Set<Skill>();
public DbSet<PlayerSkill> PlayerSkills => Set<PlayerSkill>();
// Dialogue related entities
public DbSet<Dialogue> Dialogues => Set<Dialogue>();
public DbSet<DialogueMessage> DialogueMessages => Set<DialogueMessage>();
public DbSet<DialogueMessageResponseOption> DialogueMessageResponseOptions => Set<DialogueMessageResponseOption>();
// Store and inventory
public DbSet<StoreItem> StoreItems => Set<StoreItem>();
public DbSet<UserInventoryItem> UserInventoryItems => Set<UserInventoryItem>();
public DbSet<Transaction> Transactions => Set<Transaction>();
// System entities
public DbSet<EventLog> EventLogs => Set<EventLog>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<Notification> Notifications => Set<Notification>();
// Core profile / player chain
public DbSet<Player> Players => Set<Player>();
public DbSet<Profile> Profiles => Set<Profile>();
protected override void OnModelCreating(ModelBuilder b)
{
base.OnModelCreating(b);
// Player configuration
b.Entity<Player>()
.HasIndex(p => p.UserId)
.IsUnique();
b.Entity<Player>()
.HasOne<AppUser>()
.WithOne()
.HasForeignKey<Player>(p => p.UserId)
.IsRequired();
// Rank configurations
b.Entity<Rank>()
.HasIndex(r => r.ExpNeeded)
.IsUnique();
b.Entity<Rank>()
.HasIndex(r => r.Title)
.IsUnique();
// Skill configurations
b.Entity<Skill>()
.HasIndex(s => s.Title)
.IsUnique();
// MissionCategory configurations
b.Entity<MissionCategory>()
.HasIndex(mc => mc.Title)
.IsUnique();
// Mission configurations
b.Entity<Mission>()
.HasOne(m => m.MissionCategory)
.WithMany(mc => mc.Missions)
.HasForeignKey(m => m.MissionCategoryId)
.IsRequired();
b.Entity<Mission>()
.HasOne(m => m.ParentMission)
.WithMany(m => m.ChildMissions)
.HasForeignKey(m => m.ParentMissionId)
.IsRequired(false);
// Dialogue relationship for Mission
b.Entity<Mission>()
.HasOne(m => m.Dialogue)
.WithOne(d => d.Mission)
.HasForeignKey<Mission>(m => m.DialogueId)
.IsRequired(false);
// MissionRankRule configurations
b.Entity<MissionRankRule>()
.HasOne(mrr => mrr.Mission)
.WithMany(m => m.MissionRankRules)
.HasForeignKey(mrr => mrr.MissionId);
b.Entity<MissionRankRule>()
.HasOne(mrr => mrr.Rank)
.WithMany(r => r.MissionRankRules)
.HasForeignKey(mrr => mrr.RankId);
// MissionSkillReward configurations
b.Entity<MissionSkillReward>()
.HasKey(x => new { x.MissionId, x.SkillId });
b.Entity<MissionSkillReward>()
.HasOne(msr => msr.Mission)
.WithMany(m => m.MissionSkillRewards)
.HasForeignKey(msr => msr.MissionId);
b.Entity<MissionSkillReward>()
.HasOne(msr => msr.Skill)
.WithMany(s => s.MissionSkillRewards)
.HasForeignKey(msr => msr.SkillId);
// MissionItemReward configurations
b.Entity<MissionItemReward>()
.HasOne(mir => mir.Mission)
.WithMany(m => m.MissionItemRewards)
.HasForeignKey(mir => mir.MissionId);
// RankMissionRule composite key
b.Entity<RankMissionRule>().HasKey(x => new { x.RankId, x.MissionId });
b.Entity<RankMissionRule>()
.HasOne(rmr => rmr.Rank)
.WithMany(r => r.RankMissionRules)
.HasForeignKey(rmr => rmr.RankId);
b.Entity<RankMissionRule>()
.HasOne(rmr => rmr.Mission)
.WithMany(m => m.RankMissionRules)
.HasForeignKey(rmr => rmr.MissionId);
// RankSkillRule composite key
b.Entity<RankSkillRule>().HasKey(x => new { x.RankId, x.SkillId });
b.Entity<RankSkillRule>()
.HasOne(rsr => rsr.Rank)
.WithMany(r => r.RankSkillRules)
.HasForeignKey(rsr => rsr.RankId);
b.Entity<RankSkillRule>()
.HasOne(rsr => rsr.Skill)
.WithMany(s => s.RankSkillRules)
.HasForeignKey(rsr => rsr.SkillId);
// PlayerSkill composite key
b.Entity<PlayerSkill>().HasKey(x => new { x.PlayerId, x.SkillId });
b.Entity<PlayerSkill>()
.HasOne(ps => ps.Player)
.WithMany(p => p.PlayerSkills)
.HasForeignKey(ps => ps.PlayerId);
b.Entity<PlayerSkill>()
.HasOne(ps => ps.Skill)
.WithMany(s => s.PlayerSkills)
.HasForeignKey(ps => ps.SkillId);
// PlayerMission composite key
b.Entity<PlayerMission>().HasKey(x => new { x.PlayerId, x.MissionId });
b.Entity<PlayerMission>()
.HasOne(pm => pm.Player)
.WithMany(p => p.PlayerMissions)
.HasForeignKey(pm => pm.PlayerId);
b.Entity<PlayerMission>()
.HasOne(pm => pm.Mission)
.WithMany(m => m.PlayerMissions)
.HasForeignKey(pm => pm.MissionId);
// Dialogue configurations
b.Entity<Dialogue>()
.HasOne(d => d.Mission)
.WithOne(m => m.Dialogue)
.HasForeignKey<Dialogue>(d => d.MissionId)
.IsRequired();
// DialogueMessage configurations
b.Entity<DialogueMessage>()
.HasOne(dm => dm.InitialDialogue)
.WithMany()
.HasForeignKey(dm => dm.InitialDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
b.Entity<DialogueMessage>()
.HasOne(dm => dm.InterimDialogue)
.WithMany()
.HasForeignKey(dm => dm.InterimDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
b.Entity<DialogueMessage>()
.HasOne(dm => dm.EndDialogue)
.WithMany()
.HasForeignKey(dm => dm.EndDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// DialogueMessageResponseOption configurations
b.Entity<DialogueMessageResponseOption>()
.HasOne(dmro => dmro.ParentDialogueMessage)
.WithMany(dm => dm.DialogueMessageResponseOptions)
.HasForeignKey(dmro => dmro.ParentDialogueMessageId)
.IsRequired();
b.Entity<DialogueMessageResponseOption>()
.HasOne(dmro => dmro.DestinationDialogueMessage)
.WithMany()
.HasForeignKey(dmro => dmro.DestinationDialogueMessageId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// Refresh token index unique
b.Entity<RefreshToken>().HasIndex(x => x.Token).IsUnique();
// ---------- Performance indexes ----------
b.Entity<PlayerSkill>().HasIndex(ps => ps.SkillId);
b.Entity<EventLog>().HasIndex(e => new { e.UserId, e.Type, e.CreatedAt });
b.Entity<StoreItem>().HasIndex(i => i.IsActive);
b.Entity<Transaction>().HasIndex(t => new { t.UserId, t.CreatedAt });
b.Entity<Notification>().HasIndex(n => new { n.UserId, n.IsRead, n.CreatedAt });
}
}
using LctMonolith.Models.Database;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.Data;
/// <summary>
/// Main EF Core database context for gamification module (PostgreSQL provider expected).
/// </summary>
public class AppDbContext : IdentityDbContext<AppUser, IdentityRole<Guid>, Guid>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
// Rank related entities
public DbSet<Rank> Ranks => Set<Rank>();
public DbSet<RankMissionRule> RankMissionRules => Set<RankMissionRule>();
public DbSet<RankSkillRule> RankSkillRules => Set<RankSkillRule>();
// Mission related entities
public DbSet<MissionCategory> MissionCategories => Set<MissionCategory>();
public DbSet<Mission> Missions => Set<Mission>();
public DbSet<PlayerMission> PlayerMissions => Set<PlayerMission>();
public DbSet<MissionSkillReward> MissionSkillRewards => Set<MissionSkillReward>();
public DbSet<MissionItemReward> MissionItemRewards => Set<MissionItemReward>();
public DbSet<MissionRankRule> MissionRankRules => Set<MissionRankRule>();
// Skill related entities
public DbSet<Skill> Skills => Set<Skill>();
public DbSet<PlayerSkill> PlayerSkills => Set<PlayerSkill>();
// Dialogue related entities
public DbSet<Dialogue> Dialogues => Set<Dialogue>();
public DbSet<DialogueMessage> DialogueMessages => Set<DialogueMessage>();
public DbSet<DialogueMessageResponseOption> DialogueMessageResponseOptions => Set<DialogueMessageResponseOption>();
// Store and inventory
public DbSet<StoreItem> StoreItems => Set<StoreItem>();
public DbSet<UserInventoryItem> UserInventoryItems => Set<UserInventoryItem>();
public DbSet<Transaction> Transactions => Set<Transaction>();
// System entities
public DbSet<EventLog> EventLogs => Set<EventLog>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<Notification> Notifications => Set<Notification>();
// Core profile / player chain
public DbSet<Player> Players => Set<Player>();
public DbSet<Profile> Profiles => Set<Profile>();
protected override void OnModelCreating(ModelBuilder b)
{
base.OnModelCreating(b);
// Player configuration
b.Entity<Player>()
.HasIndex(p => p.UserId)
.IsUnique();
b.Entity<Player>()
.HasOne<AppUser>()
.WithOne()
.HasForeignKey<Player>(p => p.UserId)
.IsRequired();
// Rank configurations
b.Entity<Rank>()
.HasIndex(r => r.ExpNeeded)
.IsUnique();
b.Entity<Rank>()
.HasIndex(r => r.Title)
.IsUnique();
// Skill configurations
b.Entity<Skill>()
.HasIndex(s => s.Title)
.IsUnique();
// MissionCategory configurations
b.Entity<MissionCategory>()
.HasIndex(mc => mc.Title)
.IsUnique();
// Mission configurations
b.Entity<Mission>()
.HasOne(m => m.MissionCategory)
.WithMany(mc => mc.Missions)
.HasForeignKey(m => m.MissionCategoryId)
.IsRequired();
b.Entity<Mission>()
.HasOne(m => m.ParentMission)
.WithMany(m => m.ChildMissions)
.HasForeignKey(m => m.ParentMissionId)
.IsRequired(false);
// Dialogue relationship for Mission
b.Entity<Mission>()
.HasOne(m => m.Dialogue)
.WithOne(d => d.Mission)
.HasForeignKey<Mission>(m => m.DialogueId)
.IsRequired(false);
// MissionRankRule configurations
b.Entity<MissionRankRule>()
.HasOne(mrr => mrr.Mission)
.WithMany(m => m.MissionRankRules)
.HasForeignKey(mrr => mrr.MissionId);
b.Entity<MissionRankRule>()
.HasOne(mrr => mrr.Rank)
.WithMany(r => r.MissionRankRules)
.HasForeignKey(mrr => mrr.RankId);
// MissionSkillReward configurations
b.Entity<MissionSkillReward>()
.HasKey(x => new { x.MissionId, x.SkillId });
b.Entity<MissionSkillReward>()
.HasOne(msr => msr.Mission)
.WithMany(m => m.MissionSkillRewards)
.HasForeignKey(msr => msr.MissionId);
b.Entity<MissionSkillReward>()
.HasOne(msr => msr.Skill)
.WithMany(s => s.MissionSkillRewards)
.HasForeignKey(msr => msr.SkillId);
// MissionItemReward configurations
b.Entity<MissionItemReward>()
.HasOne(mir => mir.Mission)
.WithMany(m => m.MissionItemRewards)
.HasForeignKey(mir => mir.MissionId);
// RankMissionRule composite key
b.Entity<RankMissionRule>().HasKey(x => new { x.RankId, x.MissionId });
b.Entity<RankMissionRule>()
.HasOne(rmr => rmr.Rank)
.WithMany(r => r.RankMissionRules)
.HasForeignKey(rmr => rmr.RankId);
b.Entity<RankMissionRule>()
.HasOne(rmr => rmr.Mission)
.WithMany(m => m.RankMissionRules)
.HasForeignKey(rmr => rmr.MissionId);
// RankSkillRule composite key
b.Entity<RankSkillRule>().HasKey(x => new { x.RankId, x.SkillId });
b.Entity<RankSkillRule>()
.HasOne(rsr => rsr.Rank)
.WithMany(r => r.RankSkillRules)
.HasForeignKey(rsr => rsr.RankId);
b.Entity<RankSkillRule>()
.HasOne(rsr => rsr.Skill)
.WithMany(s => s.RankSkillRules)
.HasForeignKey(rsr => rsr.SkillId);
// PlayerSkill composite key
b.Entity<PlayerSkill>().HasKey(x => new { x.PlayerId, x.SkillId });
b.Entity<PlayerSkill>()
.HasOne(ps => ps.Player)
.WithMany(p => p.PlayerSkills)
.HasForeignKey(ps => ps.PlayerId);
b.Entity<PlayerSkill>()
.HasOne(ps => ps.Skill)
.WithMany(s => s.PlayerSkills)
.HasForeignKey(ps => ps.SkillId);
// PlayerMission composite key
b.Entity<PlayerMission>().HasKey(x => new { x.PlayerId, x.MissionId });
b.Entity<PlayerMission>()
.HasOne(pm => pm.Player)
.WithMany(p => p.PlayerMissions)
.HasForeignKey(pm => pm.PlayerId);
b.Entity<PlayerMission>()
.HasOne(pm => pm.Mission)
.WithMany(m => m.PlayerMissions)
.HasForeignKey(pm => pm.MissionId);
// Dialogue configurations
b.Entity<Dialogue>()
.HasOne(d => d.Mission)
.WithOne(m => m.Dialogue)
.HasForeignKey<Dialogue>(d => d.MissionId)
.IsRequired();
// DialogueMessage configurations
b.Entity<DialogueMessage>()
.HasOne(dm => dm.InitialDialogue)
.WithMany()
.HasForeignKey(dm => dm.InitialDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
b.Entity<DialogueMessage>()
.HasOne(dm => dm.InterimDialogue)
.WithMany()
.HasForeignKey(dm => dm.InterimDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
b.Entity<DialogueMessage>()
.HasOne(dm => dm.EndDialogue)
.WithMany()
.HasForeignKey(dm => dm.EndDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// DialogueMessageResponseOption configurations
b.Entity<DialogueMessageResponseOption>()
.HasOne(dmro => dmro.ParentDialogueMessage)
.WithMany(dm => dm.DialogueMessageResponseOptions)
.HasForeignKey(dmro => dmro.ParentDialogueMessageId)
.IsRequired();
b.Entity<DialogueMessageResponseOption>()
.HasOne(dmro => dmro.DestinationDialogueMessage)
.WithMany()
.HasForeignKey(dmro => dmro.DestinationDialogueMessageId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// Refresh token index unique
b.Entity<RefreshToken>().HasIndex(x => x.Token).IsUnique();
// ---------- Performance indexes ----------
b.Entity<PlayerSkill>().HasIndex(ps => ps.SkillId);
b.Entity<EventLog>().HasIndex(e => new { e.UserId, e.Type, e.CreatedAt });
b.Entity<StoreItem>().HasIndex(i => i.IsActive);
b.Entity<Transaction>().HasIndex(t => new { t.UserId, t.CreatedAt });
b.Entity<Notification>().HasIndex(n => new { n.UserId, n.IsRead, n.CreatedAt });
}
}

View File

@@ -1,47 +1,47 @@
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Database.Data;
/// <summary>
/// Development database seeder for initial ranks, competencies, sample store items.
/// Idempotent: checks existence before inserting.
/// </summary>
public static class DbSeeder
{
public static async Task SeedAsync(AppDbContext db, CancellationToken ct = default)
{
await db.Database.EnsureCreatedAsync(ct);
if (!await db.Ranks.AnyAsync(ct))
{
var ranks = new List<Rank>
{
new() { Title = "Искатель", ExpNeeded = 0 },
new() { Title = "Пилот-кандидат", ExpNeeded = 500 },
new() { Title = "Принятый в экипаж", ExpNeeded = 1500 }
};
db.Ranks.AddRange(ranks);
Log.Information("Seeded {Count} ranks", ranks.Count);
}
if (!await db.Skills.AnyAsync(ct))
{
var comps = new[]
{
"Вера в дело","Стремление к большему","Общение","Аналитика","Командование","Юриспруденция","Трёхмерное мышление","Базовая экономика","Основы аэронавигации"
}.Select(n => new Skill { Title = n });
db.Skills.AddRange(comps);
Log.Information("Seeded competencies");
}
if (!await db.StoreItems.AnyAsync(ct))
{
db.StoreItems.AddRange(new StoreItem { Name = "Футболка Алабуга", Price = 100 }, new StoreItem { Name = "Брелок Буран", Price = 50 });
Log.Information("Seeded store items");
}
await db.SaveChangesAsync(ct);
}
}
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Database.Data;
/// <summary>
/// Development database seeder for initial ranks, competencies, sample store items.
/// Idempotent: checks existence before inserting.
/// </summary>
public static class DbSeeder
{
public static async Task SeedAsync(AppDbContext db, CancellationToken ct = default)
{
await db.Database.EnsureCreatedAsync(ct);
if (!await db.Ranks.AnyAsync(ct))
{
var ranks = new List<Rank>
{
new() { Title = "Искатель", ExpNeeded = 0 },
new() { Title = "Пилот-кандидат", ExpNeeded = 500 },
new() { Title = "Принятый в экипаж", ExpNeeded = 1500 }
};
db.Ranks.AddRange(ranks);
Log.Information("Seeded {Count} ranks", ranks.Count);
}
if (!await db.Skills.AnyAsync(ct))
{
var comps = new[]
{
"Вера в дело","Стремление к большему","Общение","Аналитика","Командование","Юриспруденция","Трёхмерное мышление","Базовая экономика","Основы аэронавигации"
}.Select(n => new Skill { Title = n });
db.Skills.AddRange(comps);
Log.Information("Seeded competencies");
}
if (!await db.StoreItems.AnyAsync(ct))
{
db.StoreItems.AddRange(new StoreItem { Name = "Футболка Алабуга", Price = 100 }, new StoreItem { Name = "Брелок Буран", Price = 50 });
Log.Information("Seeded store items");
}
await db.SaveChangesAsync(ct);
}
}

View File

@@ -1,19 +1,19 @@
namespace LctMonolith.Models.Database;
public enum EventType
{
SkillProgress = 1,
MissionStatusChanged = 2,
RankChanged = 3,
ItemPurchased = 4,
ArtifactObtained = 5,
RewardGranted = 6,
ProfileChanged = 7,
AuthCredentialsChanged = 8,
ItemReturned = 9,
ItemSold = 10
}
#if false
// Moved to Models/EventType.cs
#endif
namespace LctMonolith.Models.Database;
public enum EventType
{
SkillProgress = 1,
MissionStatusChanged = 2,
RankChanged = 3,
ItemPurchased = 4,
ArtifactObtained = 5,
RewardGranted = 6,
ProfileChanged = 7,
AuthCredentialsChanged = 8,
ItemReturned = 9,
ItemSold = 10
}
#if false
// Moved to Models/EventType.cs
#endif

View File

@@ -1,56 +1,56 @@
using System.Linq.Expressions;
using LctMonolith.Database.Data;
using Microsoft.EntityFrameworkCore;
namespace LctMonolith.Database.Repositories;
/// <summary>
/// Generic repository implementation for common CRUD and query composition.
/// </summary>
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
protected readonly AppDbContext Context;
protected readonly DbSet<TEntity> Set;
public GenericRepository(AppDbContext context)
{
Context = context;
Set = context.Set<TEntity>();
}
public IQueryable<TEntity> Query(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
params Expression<Func<TEntity, object>>[] includes)
{
IQueryable<TEntity> query = Set;
if (filter != null) query = query.Where(filter);
if (includes != null)
{
foreach (var include in includes)
query = query.Include(include);
}
if (orderBy != null) query = orderBy(query);
return query;
}
public async Task<TEntity?> GetByIdAsync(object id) => await Set.FindAsync(id) ?? null;
public ValueTask<TEntity?> FindAsync(params object[] keyValues) => Set.FindAsync(keyValues);
public async Task AddAsync(TEntity entity, CancellationToken ct = default) => await Set.AddAsync(entity, ct);
public async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default) => await Set.AddRangeAsync(entities, ct);
public void Update(TEntity entity) => Set.Update(entity);
public void Remove(TEntity entity) => Set.Remove(entity);
public async Task RemoveByIdAsync(object id, CancellationToken ct = default)
{
var entity = await Set.FindAsync([id], ct);
if (entity == null) throw new KeyNotFoundException($"Entity {typeof(TEntity).Name} id={id} not found");
Set.Remove(entity);
}
}
using System.Linq.Expressions;
using LctMonolith.Database.Data;
using Microsoft.EntityFrameworkCore;
namespace LctMonolith.Database.Repositories;
/// <summary>
/// Generic repository implementation for common CRUD and query composition.
/// </summary>
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
protected readonly AppDbContext Context;
protected readonly DbSet<TEntity> Set;
public GenericRepository(AppDbContext context)
{
Context = context;
Set = context.Set<TEntity>();
}
public IQueryable<TEntity> Query(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
params Expression<Func<TEntity, object>>[] includes)
{
IQueryable<TEntity> query = Set;
if (filter != null) query = query.Where(filter);
if (includes != null)
{
foreach (var include in includes)
query = query.Include(include);
}
if (orderBy != null) query = orderBy(query);
return query;
}
public async Task<TEntity?> GetByIdAsync(object id) => await Set.FindAsync(id) ?? null;
public ValueTask<TEntity?> FindAsync(params object[] keyValues) => Set.FindAsync(keyValues);
public async Task AddAsync(TEntity entity, CancellationToken ct = default) => await Set.AddAsync(entity, ct);
public async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default) => await Set.AddRangeAsync(entities, ct);
public void Update(TEntity entity) => Set.Update(entity);
public void Remove(TEntity entity) => Set.Remove(entity);
public async Task RemoveByIdAsync(object id, CancellationToken ct = default)
{
var entity = await Set.FindAsync([id], ct);
if (entity == null) throw new KeyNotFoundException($"Entity {typeof(TEntity).Name} id={id} not found");
Set.Remove(entity);
}
}

View File

@@ -1,25 +1,25 @@
using System.Linq.Expressions;
namespace LctMonolith.Database.Repositories;
/// <summary>
/// Generic repository abstraction for aggregate root / entity access. Read operations return IQueryable for composition.
/// </summary>
public interface IGenericRepository<TEntity> where TEntity : class
{
IQueryable<TEntity> Query(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
params Expression<Func<TEntity, object>>[] includes);
Task<TEntity?> GetByIdAsync(object id);
ValueTask<TEntity?> FindAsync(params object[] keyValues);
Task AddAsync(TEntity entity, CancellationToken ct = default);
Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);
void Update(TEntity entity);
void Remove(TEntity entity);
Task RemoveByIdAsync(object id, CancellationToken ct = default);
}
using System.Linq.Expressions;
namespace LctMonolith.Database.Repositories;
/// <summary>
/// Generic repository abstraction for aggregate root / entity access. Read operations return IQueryable for composition.
/// </summary>
public interface IGenericRepository<TEntity> where TEntity : class
{
IQueryable<TEntity> Query(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
params Expression<Func<TEntity, object>>[] includes);
Task<TEntity?> GetByIdAsync(object id);
ValueTask<TEntity?> FindAsync(params object[] keyValues);
Task AddAsync(TEntity entity, CancellationToken ct = default);
Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);
void Update(TEntity entity);
void Remove(TEntity entity);
Task RemoveByIdAsync(object id, CancellationToken ct = default);
}

View File

@@ -1,40 +1,40 @@
using LctMonolith.Database.Repositories;
using LctMonolith.Models.Database;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.UnitOfWork;
/// <summary>
/// Unit of Work aggregates repositories and transaction boundary.
/// </summary>
public interface IUnitOfWork
{
IGenericRepository<AppUser> Users { get; }
IGenericRepository<Player> Players { get; }
IGenericRepository<MissionCategory> MissionCategories { get; } // added
IGenericRepository<Rank> Ranks { get; }
IGenericRepository<RankMissionRule> RankMissionRules { get; }
IGenericRepository<RankSkillRule> RankSkillRules { get; }
IGenericRepository<Mission> Missions { get; }
IGenericRepository<PlayerMission> PlayerMissions { get; }
IGenericRepository<MissionSkillReward> MissionSkillRewards { get; }
IGenericRepository<Skill> Skills { get; }
IGenericRepository<PlayerSkill> PlayerSkills { get; }
IGenericRepository<StoreItem> StoreItems { get; }
IGenericRepository<UserInventoryItem> UserInventoryItems { get; }
IGenericRepository<Transaction> Transactions { get; }
IGenericRepository<EventLog> EventLogs { get; }
IGenericRepository<RefreshToken> RefreshTokens { get; }
IGenericRepository<Notification> Notifications { get; }
IGenericRepository<MissionItemReward> MissionItemRewards { get; } // added
IGenericRepository<MissionRankRule> MissionRankRules { get; } // added
IGenericRepository<Dialogue> Dialogues { get; }
IGenericRepository<DialogueMessage> DialogueMessages { get; }
IGenericRepository<DialogueMessageResponseOption> DialogueMessageResponseOptions { get; }
IGenericRepository<Profile> Profiles { get; }
Task<int> SaveChangesAsync(CancellationToken ct = default);
Task BeginTransactionAsync(CancellationToken ct = default);
Task CommitAsync(CancellationToken ct = default);
Task RollbackAsync(CancellationToken ct = default);
}
using LctMonolith.Database.Repositories;
using LctMonolith.Models.Database;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.UnitOfWork;
/// <summary>
/// Unit of Work aggregates repositories and transaction boundary.
/// </summary>
public interface IUnitOfWork
{
IGenericRepository<AppUser> Users { get; }
IGenericRepository<Player> Players { get; }
IGenericRepository<MissionCategory> MissionCategories { get; } // added
IGenericRepository<Rank> Ranks { get; }
IGenericRepository<RankMissionRule> RankMissionRules { get; }
IGenericRepository<RankSkillRule> RankSkillRules { get; }
IGenericRepository<Mission> Missions { get; }
IGenericRepository<PlayerMission> PlayerMissions { get; }
IGenericRepository<MissionSkillReward> MissionSkillRewards { get; }
IGenericRepository<Skill> Skills { get; }
IGenericRepository<PlayerSkill> PlayerSkills { get; }
IGenericRepository<StoreItem> StoreItems { get; }
IGenericRepository<UserInventoryItem> UserInventoryItems { get; }
IGenericRepository<Transaction> Transactions { get; }
IGenericRepository<EventLog> EventLogs { get; }
IGenericRepository<RefreshToken> RefreshTokens { get; }
IGenericRepository<Notification> Notifications { get; }
IGenericRepository<MissionItemReward> MissionItemRewards { get; } // added
IGenericRepository<MissionRankRule> MissionRankRules { get; } // added
IGenericRepository<Dialogue> Dialogues { get; }
IGenericRepository<DialogueMessage> DialogueMessages { get; }
IGenericRepository<DialogueMessageResponseOption> DialogueMessageResponseOptions { get; }
IGenericRepository<Profile> Profiles { get; }
Task<int> SaveChangesAsync(CancellationToken ct = default);
Task BeginTransactionAsync(CancellationToken ct = default);
Task CommitAsync(CancellationToken ct = default);
Task RollbackAsync(CancellationToken ct = default);
}

View File

@@ -1,110 +1,110 @@
using LctMonolith.Database.Data;
using LctMonolith.Database.Repositories;
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore.Storage;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.UnitOfWork;
/// <summary>
/// Unit of Work implementation encapsulating repositories and DB transaction scope.
/// </summary>
public class UnitOfWork : IUnitOfWork, IAsyncDisposable
{
private readonly AppDbContext _ctx;
private IDbContextTransaction? _tx;
public UnitOfWork(AppDbContext ctx)
{
_ctx = ctx;
}
private IGenericRepository<AppUser>? _users;
private IGenericRepository<Rank>? _ranks;
private IGenericRepository<RankMissionRule>? _rankMissionRules;
private IGenericRepository<RankSkillRule>? _rankSkillRules;
private IGenericRepository<Mission>? _missions;
private IGenericRepository<PlayerMission>? _playerMissions;
private IGenericRepository<MissionSkillReward>? _missionSkillRewards;
private IGenericRepository<Skill>? _skills;
private IGenericRepository<PlayerSkill>? _playerSkills;
private IGenericRepository<StoreItem>? _storeItems;
private IGenericRepository<UserInventoryItem>? _userInventoryItems;
private IGenericRepository<Transaction>? _transactions;
private IGenericRepository<EventLog>? _eventLogs;
private IGenericRepository<RefreshToken>? _refreshTokens;
private IGenericRepository<Notification>? _notifications;
private IGenericRepository<Player>? _players;
private IGenericRepository<MissionCategory>? _missionCategories;
private IGenericRepository<MissionItemReward>? _missionItemRewards;
private IGenericRepository<MissionRankRule>? _missionRankRules;
private IGenericRepository<Dialogue>? _dialogues;
private IGenericRepository<DialogueMessage>? _dialogueMessages;
private IGenericRepository<DialogueMessageResponseOption>? _dialogueMessageResponseOptions;
private IGenericRepository<Profile>? _profiles;
public IGenericRepository<AppUser> Users => _users ??= new GenericRepository<AppUser>(_ctx);
public IGenericRepository<Rank> Ranks => _ranks ??= new GenericRepository<Rank>(_ctx);
public IGenericRepository<RankMissionRule> RankMissionRules => _rankMissionRules ??= new GenericRepository<RankMissionRule>(_ctx);
public IGenericRepository<RankSkillRule> RankSkillRules => _rankSkillRules ??= new GenericRepository<RankSkillRule>(_ctx);
public IGenericRepository<Mission> Missions => _missions ??= new GenericRepository<Mission>(_ctx);
public IGenericRepository<PlayerMission> PlayerMissions => _playerMissions ??= new GenericRepository<PlayerMission>(_ctx);
public IGenericRepository<MissionSkillReward> MissionSkillRewards => _missionSkillRewards ??= new GenericRepository<MissionSkillReward>(_ctx);
public IGenericRepository<Skill> Skills => _skills ??= new GenericRepository<Skill>(_ctx);
public IGenericRepository<PlayerSkill> PlayerSkills => _playerSkills ??= new GenericRepository<PlayerSkill>(_ctx);
public IGenericRepository<StoreItem> StoreItems => _storeItems ??= new GenericRepository<StoreItem>(_ctx);
public IGenericRepository<UserInventoryItem> UserInventoryItems => _userInventoryItems ??= new GenericRepository<UserInventoryItem>(_ctx);
public IGenericRepository<Transaction> Transactions => _transactions ??= new GenericRepository<Transaction>(_ctx);
public IGenericRepository<EventLog> EventLogs => _eventLogs ??= new GenericRepository<EventLog>(_ctx);
public IGenericRepository<RefreshToken> RefreshTokens => _refreshTokens ??= new GenericRepository<RefreshToken>(_ctx);
public IGenericRepository<Notification> Notifications => _notifications ??= new GenericRepository<Notification>(_ctx);
public IGenericRepository<Player> Players => _players ??= new GenericRepository<Player>(_ctx);
public IGenericRepository<MissionCategory> MissionCategories => _missionCategories ??= new GenericRepository<MissionCategory>(_ctx);
public IGenericRepository<MissionItemReward> MissionItemRewards => _missionItemRewards ??= new GenericRepository<MissionItemReward>(_ctx);
public IGenericRepository<MissionRankRule> MissionRankRules => _missionRankRules ??= new GenericRepository<MissionRankRule>(_ctx);
public IGenericRepository<Dialogue> Dialogues => _dialogues ??= new GenericRepository<Dialogue>(_ctx);
public IGenericRepository<DialogueMessage> DialogueMessages => _dialogueMessages ??= new GenericRepository<DialogueMessage>(_ctx);
public IGenericRepository<DialogueMessageResponseOption> DialogueMessageResponseOptions => _dialogueMessageResponseOptions ??= new GenericRepository<DialogueMessageResponseOption>(_ctx);
public IGenericRepository<Profile> Profiles => _profiles ??= new GenericRepository<Profile>(_ctx);
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _ctx.SaveChangesAsync(ct);
public async Task BeginTransactionAsync(CancellationToken ct = default)
{
if (_tx != null) throw new InvalidOperationException("Transaction already started");
_tx = await _ctx.Database.BeginTransactionAsync(ct);
}
public async Task CommitAsync(CancellationToken ct = default)
{
if (_tx == null) return;
try
{
await _ctx.SaveChangesAsync(ct);
await _tx.CommitAsync(ct);
}
catch
{
await RollbackAsync(ct);
throw;
}
finally
{
await _tx.DisposeAsync();
_tx = null;
}
}
public async Task RollbackAsync(CancellationToken ct = default)
{
if (_tx == null) return;
await _tx.RollbackAsync(ct);
await _tx.DisposeAsync();
_tx = null;
}
public async ValueTask DisposeAsync()
{
if (_tx != null) await _tx.DisposeAsync();
}
}
using LctMonolith.Database.Data;
using LctMonolith.Database.Repositories;
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore.Storage;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.UnitOfWork;
/// <summary>
/// Unit of Work implementation encapsulating repositories and DB transaction scope.
/// </summary>
public class UnitOfWork : IUnitOfWork, IAsyncDisposable
{
private readonly AppDbContext _ctx;
private IDbContextTransaction? _tx;
public UnitOfWork(AppDbContext ctx)
{
_ctx = ctx;
}
private IGenericRepository<AppUser>? _users;
private IGenericRepository<Rank>? _ranks;
private IGenericRepository<RankMissionRule>? _rankMissionRules;
private IGenericRepository<RankSkillRule>? _rankSkillRules;
private IGenericRepository<Mission>? _missions;
private IGenericRepository<PlayerMission>? _playerMissions;
private IGenericRepository<MissionSkillReward>? _missionSkillRewards;
private IGenericRepository<Skill>? _skills;
private IGenericRepository<PlayerSkill>? _playerSkills;
private IGenericRepository<StoreItem>? _storeItems;
private IGenericRepository<UserInventoryItem>? _userInventoryItems;
private IGenericRepository<Transaction>? _transactions;
private IGenericRepository<EventLog>? _eventLogs;
private IGenericRepository<RefreshToken>? _refreshTokens;
private IGenericRepository<Notification>? _notifications;
private IGenericRepository<Player>? _players;
private IGenericRepository<MissionCategory>? _missionCategories;
private IGenericRepository<MissionItemReward>? _missionItemRewards;
private IGenericRepository<MissionRankRule>? _missionRankRules;
private IGenericRepository<Dialogue>? _dialogues;
private IGenericRepository<DialogueMessage>? _dialogueMessages;
private IGenericRepository<DialogueMessageResponseOption>? _dialogueMessageResponseOptions;
private IGenericRepository<Profile>? _profiles;
public IGenericRepository<AppUser> Users => _users ??= new GenericRepository<AppUser>(_ctx);
public IGenericRepository<Rank> Ranks => _ranks ??= new GenericRepository<Rank>(_ctx);
public IGenericRepository<RankMissionRule> RankMissionRules => _rankMissionRules ??= new GenericRepository<RankMissionRule>(_ctx);
public IGenericRepository<RankSkillRule> RankSkillRules => _rankSkillRules ??= new GenericRepository<RankSkillRule>(_ctx);
public IGenericRepository<Mission> Missions => _missions ??= new GenericRepository<Mission>(_ctx);
public IGenericRepository<PlayerMission> PlayerMissions => _playerMissions ??= new GenericRepository<PlayerMission>(_ctx);
public IGenericRepository<MissionSkillReward> MissionSkillRewards => _missionSkillRewards ??= new GenericRepository<MissionSkillReward>(_ctx);
public IGenericRepository<Skill> Skills => _skills ??= new GenericRepository<Skill>(_ctx);
public IGenericRepository<PlayerSkill> PlayerSkills => _playerSkills ??= new GenericRepository<PlayerSkill>(_ctx);
public IGenericRepository<StoreItem> StoreItems => _storeItems ??= new GenericRepository<StoreItem>(_ctx);
public IGenericRepository<UserInventoryItem> UserInventoryItems => _userInventoryItems ??= new GenericRepository<UserInventoryItem>(_ctx);
public IGenericRepository<Transaction> Transactions => _transactions ??= new GenericRepository<Transaction>(_ctx);
public IGenericRepository<EventLog> EventLogs => _eventLogs ??= new GenericRepository<EventLog>(_ctx);
public IGenericRepository<RefreshToken> RefreshTokens => _refreshTokens ??= new GenericRepository<RefreshToken>(_ctx);
public IGenericRepository<Notification> Notifications => _notifications ??= new GenericRepository<Notification>(_ctx);
public IGenericRepository<Player> Players => _players ??= new GenericRepository<Player>(_ctx);
public IGenericRepository<MissionCategory> MissionCategories => _missionCategories ??= new GenericRepository<MissionCategory>(_ctx);
public IGenericRepository<MissionItemReward> MissionItemRewards => _missionItemRewards ??= new GenericRepository<MissionItemReward>(_ctx);
public IGenericRepository<MissionRankRule> MissionRankRules => _missionRankRules ??= new GenericRepository<MissionRankRule>(_ctx);
public IGenericRepository<Dialogue> Dialogues => _dialogues ??= new GenericRepository<Dialogue>(_ctx);
public IGenericRepository<DialogueMessage> DialogueMessages => _dialogueMessages ??= new GenericRepository<DialogueMessage>(_ctx);
public IGenericRepository<DialogueMessageResponseOption> DialogueMessageResponseOptions => _dialogueMessageResponseOptions ??= new GenericRepository<DialogueMessageResponseOption>(_ctx);
public IGenericRepository<Profile> Profiles => _profiles ??= new GenericRepository<Profile>(_ctx);
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _ctx.SaveChangesAsync(ct);
public async Task BeginTransactionAsync(CancellationToken ct = default)
{
if (_tx != null) throw new InvalidOperationException("Transaction already started");
_tx = await _ctx.Database.BeginTransactionAsync(ct);
}
public async Task CommitAsync(CancellationToken ct = default)
{
if (_tx == null) return;
try
{
await _ctx.SaveChangesAsync(ct);
await _tx.CommitAsync(ct);
}
catch
{
await RollbackAsync(ct);
throw;
}
finally
{
await _tx.DisposeAsync();
_tx = null;
}
}
public async Task RollbackAsync(CancellationToken ct = default)
{
if (_tx == null) return;
await _tx.RollbackAsync(ct);
await _tx.DisposeAsync();
_tx = null;
}
public async ValueTask DisposeAsync()
{
if (_tx != null) await _tx.DisposeAsync();
}
}

View File

@@ -1,23 +1,23 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["LctMonolith/LctMonolith.csproj", "LctMonolith/"]
RUN dotnet restore "LctMonolith/LctMonolith.csproj"
COPY . .
WORKDIR "/src/LctMonolith"
RUN dotnet build "./LctMonolith.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./LctMonolith.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "LctMonolith.dll"]
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["LctMonolith/LctMonolith.csproj", "LctMonolith/"]
RUN dotnet restore "LctMonolith/LctMonolith.csproj"
COPY . .
WORKDIR "/src/LctMonolith"
RUN dotnet build "./LctMonolith.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./LctMonolith.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "LctMonolith.dll"]

View File

@@ -1,43 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
<!-- Serilog core and sinks -->
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
<!-- Updated to satisfy Serilog.AspNetCore dependency (>=8.0.0) -->
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
<!-- Data / Identity / Auth -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" PrivateAssets="All" />
<!-- JSON -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0" />
<!-- Updated to match transitive requirement (>=8.0.1) -->
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
<PackageReference Include="AWSSDK.S3" Version="3.7.400.6" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
<!-- Serilog core and sinks -->
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
<!-- Updated to satisfy Serilog.AspNetCore dependency (>=8.0.0) -->
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
<!-- Data / Identity / Auth -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" PrivateAssets="All" />
<!-- JSON -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0" />
<!-- Updated to match transitive requirement (>=8.0.1) -->
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
<PackageReference Include="AWSSDK.S3" Version="3.7.400.6" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
</Project>

View File

@@ -1,6 +1,6 @@
@LctMonolith_HostAddress = http://localhost:5217
GET {{LctMonolith_HostAddress}}/weatherforecast/
Accept: application/json
###
@LctMonolith_HostAddress = http://localhost:5217
GET {{LctMonolith_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -1,3 +1,3 @@
namespace LctMonolith.Models.DTO;
public class CreateMissionCategoryDto { public string Title { get; set; } = string.Empty; }
namespace LctMonolith.Models.DTO;
public class CreateMissionCategoryDto { public string Title { get; set; } = string.Empty; }

View File

@@ -1,12 +1,12 @@
namespace LctMonolith.Models.DTO;
public class CreateMissionDto
{
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public Guid MissionCategoryId { get; set; }
public Guid? ParentMissionId { get; set; }
public int ExpReward { get; set; }
public int ManaReward { get; set; }
}
namespace LctMonolith.Models.DTO;
public class CreateMissionDto
{
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public Guid MissionCategoryId { get; set; }
public Guid? ParentMissionId { get; set; }
public int ExpReward { get; set; }
public int ManaReward { get; set; }
}

View File

@@ -1,3 +1,3 @@
namespace LctMonolith.Models.DTO;
public class CreateRankDto { public string Title { get; set; } = string.Empty; public int ExpNeeded { get; set; } }
namespace LctMonolith.Models.DTO;
public class CreateRankDto { public string Title { get; set; } = string.Empty; public int ExpNeeded { get; set; } }

View File

@@ -1,3 +1,3 @@
namespace LctMonolith.Models.DTO;
public class CreateSkillDto { public string Title { get; set; } = string.Empty; }
namespace LctMonolith.Models.DTO;
public class CreateSkillDto { public string Title { get; set; } = string.Empty; }

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Models.DTO;
public class MissionCompletionResult
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public int ExperienceGained { get; set; }
public int ManaGained { get; set; }
public List<SkillProgress> SkillsProgress { get; set; } = new();
public List<Guid> UnlockedMissions { get; set; } = new();
}
namespace LctMonolith.Models.DTO;
public class MissionCompletionResult
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public int ExperienceGained { get; set; }
public int ManaGained { get; set; }
public List<SkillProgress> SkillsProgress { get; set; } = new();
public List<Guid> UnlockedMissions { get; set; } = new();
}

View File

@@ -1,8 +1,8 @@
namespace LctMonolith.Models.DTO;
public class MissionValidationResult
{
public bool IsValid { get; set; }
public string Message { get; set; } = string.Empty;
public int? SuggestedExperience { get; set; }
}
namespace LctMonolith.Models.DTO;
public class MissionValidationResult
{
public bool IsValid { get; set; }
public string Message { get; set; } = string.Empty;
public int? SuggestedExperience { get; set; }
}

View File

@@ -1,15 +1,15 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Models.DTO;
public class PlayerProgress
{
public Guid PlayerId { get; set; }
public string PlayerName { get; set; } = string.Empty;
public Rank? CurrentRank { get; set; }
public int TotalExperience { get; set; }
public int TotalMana { get; set; }
public int CompletedMissions { get; set; }
public int TotalAvailableMissions { get; set; }
public Dictionary<string, int> SkillLevels { get; set; } = new();
}
using LctMonolith.Models.Database;
namespace LctMonolith.Models.DTO;
public class PlayerProgress
{
public Guid PlayerId { get; set; }
public string PlayerName { get; set; } = string.Empty;
public Rank? CurrentRank { get; set; }
public int TotalExperience { get; set; }
public int TotalMana { get; set; }
public int CompletedMissions { get; set; }
public int TotalAvailableMissions { get; set; }
public Dictionary<string, int> SkillLevels { get; set; } = new();
}

View File

@@ -1,9 +1,9 @@
namespace LctMonolith.Models.DTO;
public class SkillProgress
{
public Guid SkillId { get; set; }
public string SkillTitle { get; set; } = string.Empty;
public int PreviousLevel { get; set; }
public int NewLevel { get; set; }
}
namespace LctMonolith.Models.DTO;
public class SkillProgress
{
public Guid SkillId { get; set; }
public string SkillTitle { get; set; } = string.Empty;
public int PreviousLevel { get; set; }
public int NewLevel { get; set; }
}

View File

@@ -1,23 +1,23 @@
using Microsoft.AspNetCore.Identity;
namespace LctMonolith.Models.Database;
public class AppUser : IdentityUser<Guid>
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateOnly? BirthDate { get; set; }
public int Experience { get; set; }
public int Mana { get; set; }
public Guid? RankId { get; set; }
public Rank? Rank { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<PlayerSkill> Competencies { get; set; } = new List<PlayerSkill>();
public ICollection<PlayerMission> Missions { get; set; } = new List<PlayerMission>();
public ICollection<UserInventoryItem> Inventory { get; set; } = new List<UserInventoryItem>();
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
public ICollection<RefreshToken> RefreshTokens { get; set; } = new List<RefreshToken>();
public ICollection<EventLog> Events { get; set; } = new List<EventLog>();
public ICollection<Notification> Notifications { get; set; } = new List<Notification>();
}
using Microsoft.AspNetCore.Identity;
namespace LctMonolith.Models.Database;
public class AppUser : IdentityUser<Guid>
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateOnly? BirthDate { get; set; }
public int Experience { get; set; }
public int Mana { get; set; }
public Guid? RankId { get; set; }
public Rank? Rank { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<PlayerSkill> Competencies { get; set; } = new List<PlayerSkill>();
public ICollection<PlayerMission> Missions { get; set; } = new List<PlayerMission>();
public ICollection<UserInventoryItem> Inventory { get; set; } = new List<UserInventoryItem>();
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
public ICollection<RefreshToken> RefreshTokens { get; set; } = new List<RefreshToken>();
public ICollection<EventLog> Events { get; set; } = new List<EventLog>();
public ICollection<Notification> Notifications { get; set; } = new List<Notification>();
}

View File

@@ -1,9 +1,9 @@
namespace LctMonolith.Models.Database;
public class AuditableEntity
{
public DateTimeOffset CreatedOn { get; set; }
public string CreatedBy { get; set; } = string.Empty;
public DateTimeOffset UpdatedOn { get; set; }
public string UpdatedBy { get; set; } = string.Empty;
}
namespace LctMonolith.Models.Database;
public class AuditableEntity
{
public DateTimeOffset CreatedOn { get; set; }
public string CreatedBy { get; set; } = string.Empty;
public DateTimeOffset UpdatedOn { get; set; }
public string UpdatedBy { get; set; } = string.Empty;
}

View File

@@ -1,16 +1,16 @@
namespace LctMonolith.Models.Database;
public class Dialogue
{
public Guid Id { get; set; }
public Guid MissionId { get; set; }
public required Mission Mission { get; set; }
public Guid InitialDialogueMessageId { get; set; }
public Dialogue? InitialDialogueMessage { get; set; }
public Guid InterimDialogueMessageId { get; set; }
public Dialogue? InterimDialogueMessage { get; set; }
public Guid EndDialogueMessageId { get; set; }
public Dialogue? EndDialogueMessage { get; set; }
}
namespace LctMonolith.Models.Database;
public class Dialogue
{
public Guid Id { get; set; }
public Guid MissionId { get; set; }
public required Mission Mission { get; set; }
public Guid InitialDialogueMessageId { get; set; }
public Dialogue? InitialDialogueMessage { get; set; }
public Guid InterimDialogueMessageId { get; set; }
public Dialogue? InterimDialogueMessage { get; set; }
public Guid EndDialogueMessageId { get; set; }
public Dialogue? EndDialogueMessage { get; set; }
}

View File

@@ -1,27 +1,27 @@
using LctMonolith.Models.Enums;
namespace LctMonolith.Models.Database;
public class DialogueMessage
{
public Guid Id { get; set; }
public Character CharacterLeft { get; set; } = Character.None;
public Character CharacterRight { get; set; } = Character.None;
public CharacterAnimation CharacterLeftAnim { get; set; } = CharacterAnimation.Neutral;
public CharacterAnimation CharacterRightAnim { get; set; } = CharacterAnimation.Neutral;
public string CharacterLeftMessage { get; set; } = string.Empty;
public string CharacterRightMessage { get; set; } = string.Empty;
public MessageStyle CharacterLeftMessageStyle { get; set; } = MessageStyle.Normal;
public MessageStyle CharacterRightMessageStyle { get; set; } = MessageStyle.Normal;
public bool AllowMessageAi { get; set; }
public string MessageAiButtonText { get; set; } = string.Empty;
public Guid InitialDialogueId { get; set; }
public Dialogue? InitialDialogue { get; set; }
public Guid InterimDialogueId { get; set; }
public Dialogue? InterimDialogue { get; set; }
public Guid EndDialogueId { get; set; }
public Dialogue? EndDialogue { get; set; }
public ICollection<DialogueMessageResponseOption> DialogueMessageResponseOptions = new List<DialogueMessageResponseOption>();
}
using LctMonolith.Models.Enums;
namespace LctMonolith.Models.Database;
public class DialogueMessage
{
public Guid Id { get; set; }
public Character CharacterLeft { get; set; } = Character.None;
public Character CharacterRight { get; set; } = Character.None;
public CharacterAnimation CharacterLeftAnim { get; set; } = CharacterAnimation.Neutral;
public CharacterAnimation CharacterRightAnim { get; set; } = CharacterAnimation.Neutral;
public string CharacterLeftMessage { get; set; } = string.Empty;
public string CharacterRightMessage { get; set; } = string.Empty;
public MessageStyle CharacterLeftMessageStyle { get; set; } = MessageStyle.Normal;
public MessageStyle CharacterRightMessageStyle { get; set; } = MessageStyle.Normal;
public bool AllowMessageAi { get; set; }
public string MessageAiButtonText { get; set; } = string.Empty;
public Guid InitialDialogueId { get; set; }
public Dialogue? InitialDialogue { get; set; }
public Guid InterimDialogueId { get; set; }
public Dialogue? InterimDialogue { get; set; }
public Guid EndDialogueId { get; set; }
public Dialogue? EndDialogue { get; set; }
public ICollection<DialogueMessageResponseOption> DialogueMessageResponseOptions = new List<DialogueMessageResponseOption>();
}

View File

@@ -1,16 +1,16 @@
using LctMonolith.Models.Enums;
namespace LctMonolith.Models.Database;
public class DialogueMessageResponseOption
{
public Guid Id { get; set; }
public string Message { get; set; } = "...";
public MessageStyle MessageStyle { get; set; } = MessageStyle.Normal;
public int z { get; set; }
public Guid ParentDialogueMessageId { get; set; }
public required DialogueMessage ParentDialogueMessage { get; set; }
public Guid DestinationDialogueMessageId { get; set; }
public DialogueMessage? DestinationDialogueMessage { get; set; }
}
using LctMonolith.Models.Enums;
namespace LctMonolith.Models.Database;
public class DialogueMessageResponseOption
{
public Guid Id { get; set; }
public string Message { get; set; } = "...";
public MessageStyle MessageStyle { get; set; } = MessageStyle.Normal;
public int z { get; set; }
public Guid ParentDialogueMessageId { get; set; }
public required DialogueMessage ParentDialogueMessage { get; set; }
public Guid DestinationDialogueMessageId { get; set; }
public DialogueMessage? DestinationDialogueMessage { get; set; }
}

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Models.Database;
public class EventLog
{
public Guid Id { get; set; } = Guid.NewGuid();
public EventType Type { get; set; }
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public string? Data { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
namespace LctMonolith.Models.Database;
public class EventLog
{
public Guid Id { get; set; } = Guid.NewGuid();
public EventType Type { get; set; }
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public string? Data { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,23 +1,23 @@
namespace LctMonolith.Models.Database;
public class Mission
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public MissionCategory? MissionCategory { get; set; }
public Guid MissionCategoryId { get; set; } // changed from long
public Mission? ParentMission { get; set; }
public Guid? ParentMissionId { get; set; } // changed from long to nullable Guid
public int ExpReward { get; set; }
public int ManaReward { get; set; }
public Guid DialogueId { get; set; }
public Dialogue? Dialogue { get; set; }
public ICollection<Mission> ChildMissions { get; set; } = new List<Mission>();
public ICollection<PlayerMission> PlayerMissions { get; set; } = new List<PlayerMission>();
public ICollection<MissionItemReward> MissionItemRewards { get; set; } = new List<MissionItemReward>();
public ICollection<MissionSkillReward> MissionSkillRewards { get; set; } = new List<MissionSkillReward>();
public ICollection<MissionRankRule> MissionRankRules { get; set; } = new List<MissionRankRule>();
public ICollection<RankMissionRule> RankMissionRules { get; set; } = new List<RankMissionRule>();
}
namespace LctMonolith.Models.Database;
public class Mission
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public MissionCategory? MissionCategory { get; set; }
public Guid MissionCategoryId { get; set; } // changed from long
public Mission? ParentMission { get; set; }
public Guid? ParentMissionId { get; set; } // changed from long to nullable Guid
public int ExpReward { get; set; }
public int ManaReward { get; set; }
public Guid DialogueId { get; set; }
public Dialogue? Dialogue { get; set; }
public ICollection<Mission> ChildMissions { get; set; } = new List<Mission>();
public ICollection<PlayerMission> PlayerMissions { get; set; } = new List<PlayerMission>();
public ICollection<MissionItemReward> MissionItemRewards { get; set; } = new List<MissionItemReward>();
public ICollection<MissionSkillReward> MissionSkillRewards { get; set; } = new List<MissionSkillReward>();
public ICollection<MissionRankRule> MissionRankRules { get; set; } = new List<MissionRankRule>();
public ICollection<RankMissionRule> RankMissionRules { get; set; } = new List<RankMissionRule>();
}

View File

@@ -1,9 +1,9 @@
namespace LctMonolith.Models.Database;
public class MissionCategory
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public ICollection<Mission> Missions { get; set; } = new List<Mission>();
}
namespace LctMonolith.Models.Database;
public class MissionCategory
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public ICollection<Mission> Missions { get; set; } = new List<Mission>();
}

View File

@@ -1,9 +1,9 @@
namespace LctMonolith.Models.Database;
public class MissionItemReward
{
public Guid Id { get; set; }
public Guid ItemId { get; set; }
public Guid MissionId { get; set; }
public required Mission Mission { get; set; }
}
namespace LctMonolith.Models.Database;
public class MissionItemReward
{
public Guid Id { get; set; }
public Guid ItemId { get; set; }
public Guid MissionId { get; set; }
public required Mission Mission { get; set; }
}

View File

@@ -1,10 +1,10 @@
namespace LctMonolith.Models.Database;
public class MissionRankRule
{
public Guid Id { get; set; }
public Guid MissionId { get; set; }
public Mission Mission { get; set; } = null!;
public Guid RankId { get; set; }
public Rank Rank { get; set; } = null!;
}
namespace LctMonolith.Models.Database;
public class MissionRankRule
{
public Guid Id { get; set; }
public Guid MissionId { get; set; }
public Mission Mission { get; set; } = null!;
public Guid RankId { get; set; }
public Rank Rank { get; set; } = null!;
}

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Models.Database;
public class MissionSkillReward
{
public Guid Id { get; set; }
public Guid MissionId { get; set; }
public Mission Mission { get; set; } = null!;
public Guid SkillId { get; set; } // changed from long
public Skill Skill { get; set; } = null!;
public int Value { get; set; }
}
namespace LctMonolith.Models.Database;
public class MissionSkillReward
{
public Guid Id { get; set; }
public Guid MissionId { get; set; }
public Mission Mission { get; set; } = null!;
public Guid SkillId { get; set; } // changed from long
public Skill Skill { get; set; } = null!;
public int Value { get; set; }
}

View File

@@ -1,14 +1,14 @@
namespace LctMonolith.Models.Database;
public class Notification
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public string Type { get; set; } = null!;
public string Title { get; set; } = null!;
public string Message { get; set; } = null!;
public bool IsRead { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ReadAt { get; set; }
}
namespace LctMonolith.Models.Database;
public class Notification
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public string Type { get; set; } = null!;
public string Title { get; set; } = null!;
public string Message { get; set; } = null!;
public bool IsRead { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ReadAt { get; set; }
}

View File

@@ -1,14 +1,14 @@
namespace LctMonolith.Models.Database;
public class Player
{
public Guid Id { get; set; }
public Guid UserId { get; set; } // 1:1 to AppUser (retain linkage)
public Guid RankId { get; set; }
public Rank? Rank { get; set; }
public int Experience { get; set; }
public int Mana { get; set; }
public ICollection<PlayerMission> PlayerMissions { get; set; } = new List<PlayerMission>();
public ICollection<PlayerSkill> PlayerSkills { get; set; } = new List<PlayerSkill>();
}
namespace LctMonolith.Models.Database;
public class Player
{
public Guid Id { get; set; }
public Guid UserId { get; set; } // 1:1 to AppUser (retain linkage)
public Guid RankId { get; set; }
public Rank? Rank { get; set; }
public int Experience { get; set; }
public int Mana { get; set; }
public ICollection<PlayerMission> PlayerMissions { get; set; } = new List<PlayerMission>();
public ICollection<PlayerSkill> PlayerSkills { get; set; } = new List<PlayerSkill>();
}

View File

@@ -1,14 +1,14 @@
namespace LctMonolith.Models.Database;
public class PlayerMission
{
public Guid Id { get; set; }
public Guid PlayerId { get; set; }
public Player Player { get; set; } = null!; // removed required
public Guid MissionId { get; set; }
public Mission Mission { get; set; } = null!; // removed required
public DateTime? Started { get; set; }
public DateTime? Completed { get; set; }
public DateTime? RewardsRedeemed { get; set; }
public int ProgressPercent { get; set; } // 0..100
}
namespace LctMonolith.Models.Database;
public class PlayerMission
{
public Guid Id { get; set; }
public Guid PlayerId { get; set; }
public Player Player { get; set; } = null!; // removed required
public Guid MissionId { get; set; }
public Mission Mission { get; set; } = null!; // removed required
public DateTime? Started { get; set; }
public DateTime? Completed { get; set; }
public DateTime? RewardsRedeemed { get; set; }
public int ProgressPercent { get; set; } // 0..100
}

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Models.Database;
public class PlayerSkill
{
public Guid Id { get; set; }
public Guid PlayerId { get; set; }
public Player Player { get; set; } = null!;
public Guid SkillId { get; set; }
public Skill Skill { get; set; } = null!;
public int Score { get; set; }
}
namespace LctMonolith.Models.Database;
public class PlayerSkill
{
public Guid Id { get; set; }
public Guid PlayerId { get; set; }
public Player Player { get; set; } = null!;
public Guid SkillId { get; set; }
public Skill Skill { get; set; } = null!;
public int Score { get; set; }
}

View File

@@ -1,21 +1,21 @@
namespace LctMonolith.Models.Database;
public class Profile
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateOnly? BirthDate { get; set; }
public string? About { get; set; }
public string? Location { get; set; }
// Avatar in S3 / MinIO
public string? AvatarS3Key { get; set; }
public string? AvatarUrl { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
namespace LctMonolith.Models.Database;
public class Profile
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateOnly? BirthDate { get; set; }
public string? About { get; set; }
public string? Location { get; set; }
// Avatar in S3 / MinIO
public string? AvatarS3Key { get; set; }
public string? AvatarUrl { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,13 +1,13 @@
namespace LctMonolith.Models.Database;
public class Rank
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public int ExpNeeded { get; set; }
public ICollection<Player> Players { get; set; } = new List<Player>();
public ICollection<MissionRankRule> MissionRankRules { get; set; } = new List<MissionRankRule>();
public ICollection<RankMissionRule> RankMissionRules { get; set; } = new List<RankMissionRule>();
public ICollection<RankSkillRule> RankSkillRules { get; set; } = new List<RankSkillRule>();
}
namespace LctMonolith.Models.Database;
public class Rank
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public int ExpNeeded { get; set; }
public ICollection<Player> Players { get; set; } = new List<Player>();
public ICollection<MissionRankRule> MissionRankRules { get; set; } = new List<MissionRankRule>();
public ICollection<RankMissionRule> RankMissionRules { get; set; } = new List<RankMissionRule>();
public ICollection<RankSkillRule> RankSkillRules { get; set; } = new List<RankSkillRule>();
}

View File

@@ -1,10 +1,10 @@
namespace LctMonolith.Models.Database;
public class RankMissionRule
{
public Guid Id { get; set; }
public Guid RankId { get; set; }
public Rank Rank { get; set; } = null!;
public Guid MissionId { get; set; }
public Mission Mission { get; set; } = null!;
}
namespace LctMonolith.Models.Database;
public class RankMissionRule
{
public Guid Id { get; set; }
public Guid RankId { get; set; }
public Rank Rank { get; set; } = null!;
public Guid MissionId { get; set; }
public Mission Mission { get; set; } = null!;
}

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Models.Database;
public class RankSkillRule
{
public Guid Id { get; set; }
public Guid RankId { get; set; }
public Rank Rank { get; set; } = null!;
public Guid SkillId { get; set; }
public Skill Skill { get; set; } = null!;
public int Min { get; set; }
}
namespace LctMonolith.Models.Database;
public class RankSkillRule
{
public Guid Id { get; set; }
public Guid RankId { get; set; }
public Rank Rank { get; set; } = null!;
public Guid SkillId { get; set; }
public Skill Skill { get; set; } = null!;
public int Min { get; set; }
}

View File

@@ -1,12 +1,12 @@
namespace LctMonolith.Models.Database;
public class RefreshToken
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Token { get; set; } = null!;
public DateTime ExpiresAt { get; set; }
public bool IsRevoked { get; set; }
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
namespace LctMonolith.Models.Database;
public class RefreshToken
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Token { get; set; } = null!;
public DateTime ExpiresAt { get; set; }
public bool IsRevoked { get; set; }
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,10 +1,10 @@
namespace LctMonolith.Models.Database;
public class Skill
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public ICollection<MissionSkillReward> MissionSkillRewards { get; set; } = new List<MissionSkillReward>();
public ICollection<RankSkillRule> RankSkillRules { get; set; } = new List<RankSkillRule>();
public ICollection<PlayerSkill> PlayerSkills { get; set; } = new List<PlayerSkill>();
}
namespace LctMonolith.Models.Database;
public class Skill
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public ICollection<MissionSkillReward> MissionSkillRewards { get; set; } = new List<MissionSkillReward>();
public ICollection<RankSkillRule> RankSkillRules { get; set; } = new List<RankSkillRule>();
public ICollection<PlayerSkill> PlayerSkills { get; set; } = new List<PlayerSkill>();
}

View File

@@ -1,12 +1,12 @@
namespace LctMonolith.Models.Database;
public class StoreItem
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = null!;
public string? Description { get; set; }
public int Price { get; set; }
public bool IsActive { get; set; } = true;
public int? Stock { get; set; }
public ICollection<UserInventoryItem> UserInventory { get; set; } = new List<UserInventoryItem>();
}
namespace LctMonolith.Models.Database;
public class StoreItem
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = null!;
public string? Description { get; set; }
public int Price { get; set; }
public bool IsActive { get; set; } = true;
public int? Stock { get; set; }
public ICollection<UserInventoryItem> UserInventory { get; set; } = new List<UserInventoryItem>();
}

View File

@@ -1,13 +1,13 @@
namespace LctMonolith.Models.Database;
public class Transaction
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public TransactionType Type { get; set; }
public Guid? StoreItemId { get; set; }
public StoreItem? StoreItem { get; set; }
public int ManaAmount { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
namespace LctMonolith.Models.Database;
public class Transaction
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public TransactionType Type { get; set; }
public Guid? StoreItemId { get; set; }
public StoreItem? StoreItem { get; set; }
public int ManaAmount { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,3 +1,3 @@
namespace LctMonolith.Models.Database;
public enum TransactionType { Purchase = 0, Return = 1, Sale = 2 }
namespace LctMonolith.Models.Database;
public enum TransactionType { Purchase = 0, Return = 1, Sale = 2 }

View File

@@ -1,12 +1,12 @@
namespace LctMonolith.Models.Database;
public class UserInventoryItem
{
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public Guid StoreItemId { get; set; }
public StoreItem StoreItem { get; set; } = null!;
public int Quantity { get; set; } = 1;
public DateTime AcquiredAt { get; set; } = DateTime.UtcNow;
public bool IsReturned { get; set; }
}
namespace LctMonolith.Models.Database;
public class UserInventoryItem
{
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public Guid StoreItemId { get; set; }
public StoreItem StoreItem { get; set; } = null!;
public int Quantity { get; set; } = 1;
public DateTime AcquiredAt { get; set; } = DateTime.UtcNow;
public bool IsReturned { get; set; }
}

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Models.Enums;
public enum Character
{
None,
Assistant,
Pilot,
Janitor,
Administrator,
Mechanic
}
namespace LctMonolith.Models.Enums;
public enum Character
{
None,
Assistant,
Pilot,
Janitor,
Administrator,
Mechanic
}

View File

@@ -1,18 +1,18 @@
namespace LctMonolith.Models.Enums;
public enum CharacterAnimation
{
Neutral,
Happy,
Laughter,
Mock,
Sad,
Crying,
Annoyed,
Angry,
Threats,
Wave,
Silhouette,
Scared,
Embarassed
}
namespace LctMonolith.Models.Enums;
public enum CharacterAnimation
{
Neutral,
Happy,
Laughter,
Mock,
Sad,
Crying,
Annoyed,
Angry,
Threats,
Wave,
Silhouette,
Scared,
Embarassed
}

View File

@@ -1,9 +1,9 @@
namespace LctMonolith.Models.Enums;
public enum MessageStyle
{
Normal,
Loud,
Think,
Action
}
namespace LctMonolith.Models.Enums;
public enum MessageStyle
{
Normal,
Loud,
Think,
Action
}

View File

@@ -1,134 +1,134 @@
using Serilog;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using LctMonolith.Models.Database; // replaced Domain.Entities
using Microsoft.AspNetCore.Identity;
using LctMonolith.Application.Middleware;
using LctMonolith.Services;
using LctMonolith.Application.Options;
using LctMonolith.Database.Data;
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Services.Contracts; // Added for JwtOptions
using LctMonolith.Application.Extensions; // added
var builder = WebApplication.CreateBuilder(args);
// Serilog configuration
builder.Host.UseSerilog((ctx, services, loggerConfig) =>
loggerConfig
.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "LctMonolith"));
// Configuration values
var connectionString = builder.Configuration.GetConnectionString("Default") ?? "Host=localhost;Port=5432;Database=lct2025;Username=postgres;Password=postgres";
var jwtSection = builder.Configuration.GetSection("Jwt");
var jwtKey = jwtSection["Key"] ?? "Dev_Insecure_Key_Change_Me";
var jwtIssuer = jwtSection["Issuer"] ?? "LctMonolith";
var jwtAudience = jwtSection["Audience"] ?? "LctMonolithAudience";
var accessMinutes = int.TryParse(jwtSection["AccessTokenMinutes"], out var m) ? m : 60;
var refreshDays = int.TryParse(jwtSection["RefreshTokenDays"], out var d) ? d : 7;
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
builder.Services.Configure<JwtOptions>(o =>
{
o.Key = jwtKey;
o.Issuer = jwtIssuer;
o.Audience = jwtAudience;
o.AccessTokenMinutes = accessMinutes;
o.RefreshTokenDays = refreshDays;
});
// DbContext
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseNpgsql(connectionString));
// Identity Core
builder.Services.AddIdentityCore<AppUser>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireLowercase = false;
options.Password.RequiredLength = 6;
})
.AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<AppDbContext>()
.AddSignInManager<SignInManager<AppUser>>()
.AddDefaultTokenProviders();
// Authentication & JWT
builder.Services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
IssuerSigningKey = signingKey,
ClockSkew = TimeSpan.FromMinutes(2)
};
});
// Controllers + NewtonsoftJson
builder.Services.AddControllers()
.AddNewtonsoftJson();
// OpenAPI
builder.Services.AddOpenApi();
// Health checks
builder.Services.AddHealthChecks();
// Remove individual service registrations and replace with extension
// builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
// builder.Services.AddScoped<ITokenService, TokenService>();
// builder.Services.AddScoped<IStoreService, StoreService>();
// builder.Services.AddScoped<INotificationService, NotificationService>();
// builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
builder.Services.AddApplicationServices(builder.Configuration);
// CORS
builder.Services.AddCors(p => p.AddDefaultPolicy(policy =>
policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
await DbSeeder.SeedAsync(db); // seed dev data
}
app.UseSerilogRequestLogging();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseCors();
app.UseHttpsRedirection();
app.UseErrorHandling();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false });
app.MapHealthChecks("/health/ready");
app.Run();
using Serilog;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using LctMonolith.Models.Database; // replaced Domain.Entities
using Microsoft.AspNetCore.Identity;
using LctMonolith.Application.Middleware;
using LctMonolith.Services;
using LctMonolith.Application.Options;
using LctMonolith.Database.Data;
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Services.Contracts; // Added for JwtOptions
using LctMonolith.Application.Extensions; // added
var builder = WebApplication.CreateBuilder(args);
// Serilog configuration
builder.Host.UseSerilog((ctx, services, loggerConfig) =>
loggerConfig
.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "LctMonolith"));
// Configuration values
var connectionString = builder.Configuration.GetConnectionString("Default") ?? "Host=localhost;Port=5432;Database=lct2025;Username=postgres;Password=postgres";
var jwtSection = builder.Configuration.GetSection("Jwt");
var jwtKey = jwtSection["Key"] ?? "Dev_Insecure_Key_Change_Me";
var jwtIssuer = jwtSection["Issuer"] ?? "LctMonolith";
var jwtAudience = jwtSection["Audience"] ?? "LctMonolithAudience";
var accessMinutes = int.TryParse(jwtSection["AccessTokenMinutes"], out var m) ? m : 60;
var refreshDays = int.TryParse(jwtSection["RefreshTokenDays"], out var d) ? d : 7;
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
builder.Services.Configure<JwtOptions>(o =>
{
o.Key = jwtKey;
o.Issuer = jwtIssuer;
o.Audience = jwtAudience;
o.AccessTokenMinutes = accessMinutes;
o.RefreshTokenDays = refreshDays;
});
// DbContext
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseNpgsql(connectionString));
// Identity Core
builder.Services.AddIdentityCore<AppUser>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireLowercase = false;
options.Password.RequiredLength = 6;
})
.AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<AppDbContext>()
.AddSignInManager<SignInManager<AppUser>>()
.AddDefaultTokenProviders();
// Authentication & JWT
builder.Services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
IssuerSigningKey = signingKey,
ClockSkew = TimeSpan.FromMinutes(2)
};
});
// Controllers + NewtonsoftJson
builder.Services.AddControllers()
.AddNewtonsoftJson();
// OpenAPI
builder.Services.AddOpenApi();
// Health checks
builder.Services.AddHealthChecks();
// Remove individual service registrations and replace with extension
// builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
// builder.Services.AddScoped<ITokenService, TokenService>();
// builder.Services.AddScoped<IStoreService, StoreService>();
// builder.Services.AddScoped<INotificationService, NotificationService>();
// builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
builder.Services.AddApplicationServices(builder.Configuration);
// CORS
builder.Services.AddCors(p => p.AddDefaultPolicy(policy =>
policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
await DbSeeder.SeedAsync(db); // seed dev data
}
app.UseSerilogRequestLogging();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseCors();
app.UseHttpsRedirection();
app.UseErrorHandling();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false });
app.MapHealthChecks("/health/ready");
app.Run();

View File

@@ -1,23 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5217",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7144;http://localhost:5217",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5217",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7144;http://localhost:5217",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,41 +1,41 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Models;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// Provides aggregated analytics metrics for dashboards.
/// </summary>
public class AnalyticsService : IAnalyticsService
{
private readonly IUnitOfWork _uow;
public AnalyticsService(IUnitOfWork uow) => _uow = uow;
public async Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default)
{
try
{
var totalUsers = await _uow.Users.Query().CountAsync(ct);
var totalMissions = await _uow.Missions.Query().CountAsync(ct);
var totalStoreItems = await _uow.StoreItems.Query().CountAsync(ct);
var totalExperience = await _uow.Players.Query().SumAsync(p => (long)p.Experience, ct);
var completedMissions = await _uow.PlayerMissions.Query(pm => pm.Completed != null).CountAsync(ct);
return new AnalyticsSummary
{
TotalUsers = totalUsers,
TotalMissions = totalMissions,
TotalStoreItems = totalStoreItems,
TotalExperience = totalExperience,
CompletedMissions = completedMissions
};
}
catch (Exception ex)
{
Log.Error(ex, "Failed to build analytics summary");
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Models;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// Provides aggregated analytics metrics for dashboards.
/// </summary>
public class AnalyticsService : IAnalyticsService
{
private readonly IUnitOfWork _uow;
public AnalyticsService(IUnitOfWork uow) => _uow = uow;
public async Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default)
{
try
{
var totalUsers = await _uow.Users.Query().CountAsync(ct);
var totalMissions = await _uow.Missions.Query().CountAsync(ct);
var totalStoreItems = await _uow.StoreItems.Query().CountAsync(ct);
var totalExperience = await _uow.Players.Query().SumAsync(p => (long)p.Experience, ct);
var completedMissions = await _uow.PlayerMissions.Query(pm => pm.Completed != null).CountAsync(ct);
return new AnalyticsSummary
{
TotalUsers = totalUsers,
TotalMissions = totalMissions,
TotalStoreItems = totalStoreItems,
TotalExperience = totalExperience,
CompletedMissions = completedMissions
};
}
catch (Exception ex)
{
Log.Error(ex, "Failed to build analytics summary");
throw;
}
}
}

View File

@@ -1,8 +1,8 @@
using LctMonolith.Services.Models;
namespace LctMonolith.Services;
public interface IAnalyticsService
{
Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default);
}
using LctMonolith.Services.Models;
namespace LctMonolith.Services;
public interface IAnalyticsService
{
Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default);
}

View File

@@ -1,12 +1,12 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IDialogueService
{
Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId);
Task<Dialogue> CreateDialogueAsync(Dialogue dialogue);
Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId);
Task<IEnumerable<DialogueMessageResponseOption>> GetResponseOptionsAsync(Guid messageId);
Task<DialogueMessage?> ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IDialogueService
{
Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId);
Task<Dialogue> CreateDialogueAsync(Dialogue dialogue);
Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId);
Task<IEnumerable<DialogueMessageResponseOption>> GetResponseOptionsAsync(Guid messageId);
Task<DialogueMessage?> ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId);
}

View File

@@ -1,8 +1,8 @@
namespace LctMonolith.Services.Interfaces;
public interface IFileStorageService
{
Task<string> UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default);
Task DeleteAsync(string key, CancellationToken ct = default);
Task<string> GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default);
}
namespace LctMonolith.Services.Interfaces;
public interface IFileStorageService
{
Task<string> UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default);
Task DeleteAsync(string key, CancellationToken ct = default);
Task<string> GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default);
}

View File

@@ -1,8 +1,8 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface IInventoryService
{
Task<IEnumerable<UserInventoryItem>> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface IInventoryService
{
Task<IEnumerable<UserInventoryItem>> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default);
}

View File

@@ -1,14 +1,14 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IMissionCategoryService
{
// CRUD should be enough
Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId);
Task<MissionCategory?> GetCategoryByTitleAsync(string title);
Task<IEnumerable<MissionCategory>> GetAllCategoriesAsync();
Task<MissionCategory> CreateCategoryAsync(MissionCategory category);
Task<MissionCategory> UpdateCategoryAsync(MissionCategory category);
Task<bool> DeleteCategoryAsync(Guid categoryId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IMissionCategoryService
{
// CRUD should be enough
Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId);
Task<MissionCategory?> GetCategoryByTitleAsync(string title);
Task<IEnumerable<MissionCategory>> GetAllCategoriesAsync();
Task<MissionCategory> CreateCategoryAsync(MissionCategory category);
Task<MissionCategory> UpdateCategoryAsync(MissionCategory category);
Task<bool> DeleteCategoryAsync(Guid categoryId);
}

View File

@@ -1,17 +1,17 @@
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
namespace LctMonolith.Services.Interfaces;
public interface IMissionService
{
Task<Mission?> GetMissionByIdAsync(Guid missionId);
Task<IEnumerable<Mission>> GetMissionsByCategoryAsync(Guid categoryId);
Task<IEnumerable<Mission>> GetAvailableMissionsForPlayerAsync(Guid playerId);
Task<IEnumerable<Mission>> GetChildMissionsAsync(Guid parentMissionId);
Task<Mission> CreateMissionAsync(Mission mission);
Task<Mission> UpdateMissionAsync(Mission mission);
Task<bool> DeleteMissionAsync(Guid missionId);
Task<bool> IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId);
Task<MissionCompletionResult> CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null);
}
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
namespace LctMonolith.Services.Interfaces;
public interface IMissionService
{
Task<Mission?> GetMissionByIdAsync(Guid missionId);
Task<IEnumerable<Mission>> GetMissionsByCategoryAsync(Guid categoryId);
Task<IEnumerable<Mission>> GetAvailableMissionsForPlayerAsync(Guid playerId);
Task<IEnumerable<Mission>> GetChildMissionsAsync(Guid parentMissionId);
Task<Mission> CreateMissionAsync(Mission mission);
Task<Mission> UpdateMissionAsync(Mission mission);
Task<bool> DeleteMissionAsync(Guid missionId);
Task<bool> IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId);
Task<MissionCompletionResult> CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null);
}

View File

@@ -1,12 +1,12 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface INotificationService
{
Task<Notification> CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default);
Task<IEnumerable<Notification>> GetUnreadAsync(Guid userId, CancellationToken ct = default);
Task<IEnumerable<Notification>> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default);
Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default);
Task<int> MarkAllReadAsync(Guid userId, CancellationToken ct = default);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface INotificationService
{
Task<Notification> CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default);
Task<IEnumerable<Notification>> GetUnreadAsync(Guid userId, CancellationToken ct = default);
Task<IEnumerable<Notification>> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default);
Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default);
Task<int> MarkAllReadAsync(Guid userId, CancellationToken ct = default);
}

View File

@@ -1,14 +1,14 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IPlayerService
{
Task<Player?> GetPlayerByUserIdAsync(string userId);
Task<Player> CreatePlayerAsync(string userId, string username);
Task<Player> UpdatePlayerRankAsync(Guid playerId, Guid newRankId);
Task<Player> AddPlayerExperienceAsync(Guid playerId, int experience);
Task<Player> AddPlayerManaAsync(Guid playerId, int mana);
Task<IEnumerable<Player>> GetTopPlayersAsync(int topCount, TimeSpan timeFrame);
Task<Player> GetPlayerWithProgressAsync(Guid playerId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IPlayerService
{
Task<Player?> GetPlayerByUserIdAsync(string userId);
Task<Player> CreatePlayerAsync(string userId, string username);
Task<Player> UpdatePlayerRankAsync(Guid playerId, Guid newRankId);
Task<Player> AddPlayerExperienceAsync(Guid playerId, int experience);
Task<Player> AddPlayerManaAsync(Guid playerId, int mana);
Task<IEnumerable<Player>> GetTopPlayersAsync(int topCount, TimeSpan timeFrame);
Task<Player> GetPlayerWithProgressAsync(Guid playerId);
}

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Services.Interfaces;
using LctMonolith.Models.Database;
public interface IProfileService
{
Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default);
Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default);
Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default);
Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default);
}
namespace LctMonolith.Services.Interfaces;
using LctMonolith.Models.Database;
public interface IProfileService
{
Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default);
Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default);
Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default);
Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default);
}

View File

@@ -1,14 +1,14 @@
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
namespace LctMonolith.Services.Interfaces;
public interface IProgressTrackingService
{
Task<PlayerMission> StartMissionAsync(Guid missionId, Guid playerId);
Task<PlayerMission> UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null);
Task<PlayerMission> CompleteMissionAsync(Guid playerMissionId, object? proof = null);
Task<IEnumerable<PlayerMission>> GetPlayerMissionsAsync(Guid playerId);
Task<PlayerMission?> GetPlayerMissionAsync(Guid playerId, Guid missionId);
Task<PlayerProgress> GetPlayerOverallProgressAsync(Guid playerId);
}
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
namespace LctMonolith.Services.Interfaces;
public interface IProgressTrackingService
{
Task<PlayerMission> StartMissionAsync(Guid missionId, Guid playerId);
Task<PlayerMission> UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null);
Task<PlayerMission> CompleteMissionAsync(Guid playerMissionId, object? proof = null);
Task<IEnumerable<PlayerMission>> GetPlayerMissionsAsync(Guid playerId);
Task<PlayerMission?> GetPlayerMissionAsync(Guid playerId, Guid missionId);
Task<PlayerProgress> GetPlayerOverallProgressAsync(Guid playerId);
}

View File

@@ -1,15 +1,15 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRankService
{
Task<Rank?> GetRankByIdAsync(Guid rankId);
Task<Rank?> GetRankByTitleAsync(string title);
Task<IEnumerable<Rank>> GetAllRanksAsync();
Task<Rank> CreateRankAsync(Rank rank);
Task<Rank> UpdateRankAsync(Rank rank);
Task<bool> DeleteRankAsync(Guid rankId);
Task<bool> CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId);
Task<Rank?> GetNextRankAsync(Guid currentRankId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRankService
{
Task<Rank?> GetRankByIdAsync(Guid rankId);
Task<Rank?> GetRankByTitleAsync(string title);
Task<IEnumerable<Rank>> GetAllRanksAsync();
Task<Rank> CreateRankAsync(Rank rank);
Task<Rank> UpdateRankAsync(Rank rank);
Task<bool> DeleteRankAsync(Guid rankId);
Task<bool> CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId);
Task<Rank?> GetNextRankAsync(Guid currentRankId);
}

View File

@@ -1,11 +1,11 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRewardService
{
Task<IEnumerable<MissionSkillReward>> GetMissionSkillRewardsAsync(Guid missionId);
Task<IEnumerable<MissionItemReward>> GetMissionItemRewardsAsync(Guid missionId);
Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId);
Task<bool> CanClaimRewardAsync(Guid rewardId, Guid playerId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRewardService
{
Task<IEnumerable<MissionSkillReward>> GetMissionSkillRewardsAsync(Guid missionId);
Task<IEnumerable<MissionItemReward>> GetMissionItemRewardsAsync(Guid missionId);
Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId);
Task<bool> CanClaimRewardAsync(Guid rewardId, Guid playerId);
}

View File

@@ -1,10 +1,10 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRuleValidationService
{
Task<bool> ValidateMissionRankRulesAsync(Guid missionId, Guid playerId);
Task<bool> ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId);
Task<IEnumerable<MissionRankRule>> GetApplicableRankRulesAsync(Guid missionId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRuleValidationService
{
Task<bool> ValidateMissionRankRulesAsync(Guid missionId, Guid playerId);
Task<bool> ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId);
Task<IEnumerable<MissionRankRule>> GetApplicableRankRulesAsync(Guid missionId);
}

View File

@@ -1,16 +1,16 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface ISkillService
{
Task<Skill?> GetSkillByIdAsync(Guid skillId);
Task<Skill?> GetSkillByTitleAsync(string title);
Task<IEnumerable<Skill>> GetAllSkillsAsync();
Task<Skill> CreateSkillAsync(Skill skill);
Task<Skill> UpdateSkillAsync(Skill skill);
Task<bool> DeleteSkillAsync(Guid skillId);
Task<PlayerSkill> UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level);
Task<IEnumerable<PlayerSkill>> GetPlayerSkillsAsync(Guid playerId);
Task<int> GetPlayerSkillLevelAsync(Guid playerId, Guid skillId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface ISkillService
{
Task<Skill?> GetSkillByIdAsync(Guid skillId);
Task<Skill?> GetSkillByTitleAsync(string title);
Task<IEnumerable<Skill>> GetAllSkillsAsync();
Task<Skill> CreateSkillAsync(Skill skill);
Task<Skill> UpdateSkillAsync(Skill skill);
Task<bool> DeleteSkillAsync(Guid skillId);
Task<PlayerSkill> UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level);
Task<IEnumerable<PlayerSkill>> GetPlayerSkillsAsync(Guid playerId);
Task<int> GetPlayerSkillLevelAsync(Guid playerId, Guid skillId);
}

View File

@@ -1,9 +1,9 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface IStoreService
{
Task<IEnumerable<StoreItem>> GetActiveItemsAsync(CancellationToken ct = default);
Task<UserInventoryItem> PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface IStoreService
{
Task<IEnumerable<StoreItem>> GetActiveItemsAsync(CancellationToken ct = default);
Task<UserInventoryItem> PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default);
}

View File

@@ -1,11 +1,11 @@
using LctMonolith.Models.Database;
using LctMonolith.Services.Models;
namespace LctMonolith.Services.Contracts;
public interface ITokenService
{
Task<TokenPair> IssueAsync(AppUser user, CancellationToken ct = default);
Task<TokenPair> RefreshAsync(string refreshToken, CancellationToken ct = default);
Task RevokeAsync(string refreshToken, CancellationToken ct = default);
}
using LctMonolith.Models.Database;
using LctMonolith.Services.Models;
namespace LctMonolith.Services.Contracts;
public interface ITokenService
{
Task<TokenPair> IssueAsync(AppUser user, CancellationToken ct = default);
Task<TokenPair> RefreshAsync(string refreshToken, CancellationToken ct = default);
Task RevokeAsync(string refreshToken, CancellationToken ct = default);
}

View File

@@ -1,55 +1,55 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class DialogueService : IDialogueService
{
private readonly IUnitOfWork _uow;
public DialogueService(IUnitOfWork uow) => _uow = uow;
public async Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId)
{
try { return await _uow.Dialogues.Query(d => d.MissionId == missionId).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetDialogueByMissionIdAsync failed {MissionId}", missionId); throw; }
}
public async Task<Dialogue> CreateDialogueAsync(Dialogue dialogue)
{
try
{
dialogue.Id = Guid.NewGuid();
await _uow.Dialogues.AddAsync(dialogue);
await _uow.SaveChangesAsync();
return dialogue;
}
catch (Exception ex) { Log.Error(ex, "CreateDialogueAsync failed {MissionId}", dialogue.MissionId); throw; }
}
public async Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId)
{
try { return await _uow.DialogueMessages.GetByIdAsync(messageId); }
catch (Exception ex) { Log.Error(ex, "GetDialogueMessageByIdAsync failed {MessageId}", messageId); throw; }
}
public async Task<IEnumerable<DialogueMessageResponseOption>> GetResponseOptionsAsync(Guid messageId)
{
try { return await _uow.DialogueMessageResponseOptions.Query(o => o.ParentDialogueMessageId == messageId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetResponseOptionsAsync failed {MessageId}", messageId); throw; }
}
public async Task<DialogueMessage?> ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId)
{
try
{
var option = await _uow.DialogueMessageResponseOptions.Query(o => o.Id == responseOptionId && o.ParentDialogueMessageId == messageId).FirstOrDefaultAsync();
if (option == null) return null;
if (option.DestinationDialogueMessageId == null) return null; // end branch
return await _uow.DialogueMessages.GetByIdAsync(option.DestinationDialogueMessageId);
}
catch (Exception ex) { Log.Error(ex, "ProcessDialogueResponseAsync failed {MessageId} {ResponseId}", messageId, responseOptionId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class DialogueService : IDialogueService
{
private readonly IUnitOfWork _uow;
public DialogueService(IUnitOfWork uow) => _uow = uow;
public async Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId)
{
try { return await _uow.Dialogues.Query(d => d.MissionId == missionId).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetDialogueByMissionIdAsync failed {MissionId}", missionId); throw; }
}
public async Task<Dialogue> CreateDialogueAsync(Dialogue dialogue)
{
try
{
dialogue.Id = Guid.NewGuid();
await _uow.Dialogues.AddAsync(dialogue);
await _uow.SaveChangesAsync();
return dialogue;
}
catch (Exception ex) { Log.Error(ex, "CreateDialogueAsync failed {MissionId}", dialogue.MissionId); throw; }
}
public async Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId)
{
try { return await _uow.DialogueMessages.GetByIdAsync(messageId); }
catch (Exception ex) { Log.Error(ex, "GetDialogueMessageByIdAsync failed {MessageId}", messageId); throw; }
}
public async Task<IEnumerable<DialogueMessageResponseOption>> GetResponseOptionsAsync(Guid messageId)
{
try { return await _uow.DialogueMessageResponseOptions.Query(o => o.ParentDialogueMessageId == messageId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetResponseOptionsAsync failed {MessageId}", messageId); throw; }
}
public async Task<DialogueMessage?> ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId)
{
try
{
var option = await _uow.DialogueMessageResponseOptions.Query(o => o.Id == responseOptionId && o.ParentDialogueMessageId == messageId).FirstOrDefaultAsync();
if (option == null) return null;
if (option.DestinationDialogueMessageId == null) return null; // end branch
return await _uow.DialogueMessages.GetByIdAsync(option.DestinationDialogueMessageId);
}
catch (Exception ex) { Log.Error(ex, "ProcessDialogueResponseAsync failed {MessageId} {ResponseId}", messageId, responseOptionId); throw; }
}
}

View File

@@ -1,26 +1,26 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class InventoryService : IInventoryService
{
private readonly IUnitOfWork _uow;
public InventoryService(IUnitOfWork uow) => _uow = uow;
public async Task<IEnumerable<UserInventoryItem>> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default)
{
try
{
return await _uow.UserInventoryItems.Query(i => i.UserId == userId, null, i => i.StoreItem).ToListAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "GetStoreInventoryAsync failed {UserId}", userId);
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class InventoryService : IInventoryService
{
private readonly IUnitOfWork _uow;
public InventoryService(IUnitOfWork uow) => _uow = uow;
public async Task<IEnumerable<UserInventoryItem>> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default)
{
try
{
return await _uow.UserInventoryItems.Query(i => i.UserId == userId, null, i => i.StoreItem).ToListAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "GetStoreInventoryAsync failed {UserId}", userId);
throw;
}
}
}

View File

@@ -1,49 +1,49 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class MissionCategoryService : IMissionCategoryService
{
private readonly IUnitOfWork _uow;
public MissionCategoryService(IUnitOfWork uow) => _uow = uow;
public async Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId)
{
try { return await _uow.MissionCategories.GetByIdAsync(categoryId); }
catch (Exception ex) { Log.Error(ex, "GetCategoryByIdAsync failed {CategoryId}", categoryId); throw; }
}
public async Task<MissionCategory?> GetCategoryByTitleAsync(string title)
{
try { return await _uow.MissionCategories.Query(c => c.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetCategoryByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<MissionCategory>> GetAllCategoriesAsync()
{
try { return await _uow.MissionCategories.Query().OrderBy(c => c.Title).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllCategoriesAsync failed"); throw; }
}
public async Task<MissionCategory> CreateCategoryAsync(MissionCategory category)
{
try { category.Id = Guid.NewGuid(); await _uow.MissionCategories.AddAsync(category); await _uow.SaveChangesAsync(); return category; }
catch (Exception ex) { Log.Error(ex, "CreateCategoryAsync failed {Title}", category.Title); throw; }
}
public async Task<MissionCategory> UpdateCategoryAsync(MissionCategory category)
{
try { _uow.MissionCategories.Update(category); await _uow.SaveChangesAsync(); return category; }
catch (Exception ex) { Log.Error(ex, "UpdateCategoryAsync failed {Id}", category.Id); throw; }
}
public async Task<bool> DeleteCategoryAsync(Guid categoryId)
{
try { var c = await _uow.MissionCategories.GetByIdAsync(categoryId); if (c == null) return false; _uow.MissionCategories.Remove(c); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteCategoryAsync failed {CategoryId}", categoryId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class MissionCategoryService : IMissionCategoryService
{
private readonly IUnitOfWork _uow;
public MissionCategoryService(IUnitOfWork uow) => _uow = uow;
public async Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId)
{
try { return await _uow.MissionCategories.GetByIdAsync(categoryId); }
catch (Exception ex) { Log.Error(ex, "GetCategoryByIdAsync failed {CategoryId}", categoryId); throw; }
}
public async Task<MissionCategory?> GetCategoryByTitleAsync(string title)
{
try { return await _uow.MissionCategories.Query(c => c.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetCategoryByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<MissionCategory>> GetAllCategoriesAsync()
{
try { return await _uow.MissionCategories.Query().OrderBy(c => c.Title).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllCategoriesAsync failed"); throw; }
}
public async Task<MissionCategory> CreateCategoryAsync(MissionCategory category)
{
try { category.Id = Guid.NewGuid(); await _uow.MissionCategories.AddAsync(category); await _uow.SaveChangesAsync(); return category; }
catch (Exception ex) { Log.Error(ex, "CreateCategoryAsync failed {Title}", category.Title); throw; }
}
public async Task<MissionCategory> UpdateCategoryAsync(MissionCategory category)
{
try { _uow.MissionCategories.Update(category); await _uow.SaveChangesAsync(); return category; }
catch (Exception ex) { Log.Error(ex, "UpdateCategoryAsync failed {Id}", category.Id); throw; }
}
public async Task<bool> DeleteCategoryAsync(Guid categoryId)
{
try { var c = await _uow.MissionCategories.GetByIdAsync(categoryId); if (c == null) return false; _uow.MissionCategories.Remove(c); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteCategoryAsync failed {CategoryId}", categoryId); throw; }
}
}

View File

@@ -1,165 +1,165 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class MissionService : IMissionService
{
private readonly IUnitOfWork _uow;
private readonly IRewardService _rewardService;
private readonly IRuleValidationService _ruleValidationService;
public MissionService(IUnitOfWork uow, IRewardService rewardService, IRuleValidationService ruleValidationService)
{
_uow = uow;
_rewardService = rewardService;
_ruleValidationService = ruleValidationService;
}
public async Task<Mission?> GetMissionByIdAsync(Guid missionId)
{
try { return await _uow.Missions.GetByIdAsync(missionId); }
catch (Exception ex) { Log.Error(ex, "GetMissionByIdAsync failed {MissionId}", missionId); throw; }
}
public async Task<IEnumerable<Mission>> GetMissionsByCategoryAsync(Guid categoryId)
{
try { return await _uow.Missions.Query(m => m.MissionCategoryId == categoryId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetMissionsByCategoryAsync failed {Category}", categoryId); throw; }
}
public async Task<IEnumerable<Mission>> GetAvailableMissionsForPlayerAsync(Guid playerId)
{
try
{
var missions = await _uow.Missions.Query().ToListAsync();
var result = new List<Mission>();
foreach (var m in missions)
{
if (await IsMissionAvailableForPlayerAsync(m.Id, playerId)) result.Add(m);
}
return result;
}
catch (Exception ex) { Log.Error(ex, "GetAvailableMissionsForPlayerAsync failed {Player}", playerId); throw; }
}
public async Task<IEnumerable<Mission>> GetChildMissionsAsync(Guid parentMissionId)
{
try { return await _uow.Missions.Query(m => m.ParentMissionId == parentMissionId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetChildMissionsAsync failed {ParentMission}", parentMissionId); throw; }
}
public async Task<Mission> CreateMissionAsync(Mission mission)
{
try
{
mission.Id = Guid.NewGuid();
await _uow.Missions.AddAsync(mission);
await _uow.SaveChangesAsync();
return mission;
}
catch (Exception ex) { Log.Error(ex, "CreateMissionAsync failed {Title}", mission.Title); throw; }
}
public async Task<Mission> UpdateMissionAsync(Mission mission)
{
try { _uow.Missions.Update(mission); await _uow.SaveChangesAsync(); return mission; }
catch (Exception ex) { Log.Error(ex, "UpdateMissionAsync failed {MissionId}", mission.Id); throw; }
}
public async Task<bool> DeleteMissionAsync(Guid missionId)
{
try { var m = await _uow.Missions.GetByIdAsync(missionId); if (m == null) return false; _uow.Missions.Remove(m); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteMissionAsync failed {MissionId}", missionId); throw; }
}
public async Task<bool> IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId)
{
try
{
var mission = await _uow.Missions.GetByIdAsync(missionId);
if (mission == null) return false;
// rule validation
if (!await _ruleValidationService.ValidateMissionRankRulesAsync(missionId, playerId)) return false;
// already completed? then not available
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId && pm.Completed != null).AnyAsync();
if (completed) return false;
// if parent mission required ensure parent completed
if (mission.ParentMissionId.HasValue)
{
var parentDone = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == mission.ParentMissionId && pm.Completed != null).AnyAsync();
if (!parentDone) return false;
}
return true;
}
catch (Exception ex) { Log.Error(ex, "IsMissionAvailableForPlayerAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<MissionCompletionResult> CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null)
{
try
{
if (!await IsMissionAvailableForPlayerAsync(missionId, playerId))
{
return new MissionCompletionResult { Success = false, Message = "Mission not available" };
}
var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found");
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
// snapshot skill levels before
var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync();
var beforeSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
// mark PlayerMission
var pm = await _uow.PlayerMissions.Query(x => x.PlayerId == playerId && x.MissionId == missionId).FirstOrDefaultAsync();
if (pm == null)
{
pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow };
await _uow.PlayerMissions.AddAsync(pm);
}
pm.Completed = DateTime.UtcNow;
pm.ProgressPercent = 100;
await _uow.SaveChangesAsync();
var prevExp = player.Experience;
var prevMana = player.Mana;
// distribute rewards (XP/Mana/Skills/Items)
await _rewardService.DistributeMissionRewardsAsync(missionId, playerId);
await _uow.SaveChangesAsync();
// build skill progress
var afterSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
var skillProgress = new List<SkillProgress>();
foreach (var r in skillRewards)
{
var before = beforeSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? 0;
var after = afterSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? before;
if (after != before)
{
var skill = await _uow.Skills.GetByIdAsync(r.SkillId);
skillProgress.Add(new SkillProgress { SkillId = r.SkillId, SkillTitle = skill?.Title ?? string.Empty, PreviousLevel = before, NewLevel = after });
}
}
return new MissionCompletionResult
{
Success = true,
Message = "Mission completed",
ExperienceGained = player.Experience - prevExp,
ManaGained = player.Mana - prevMana,
SkillsProgress = skillProgress,
UnlockedMissions = (await _uow.Missions.Query(m => m.ParentMissionId == missionId).Select(m => m.Id).ToListAsync())
};
}
catch (Exception ex)
{
Log.Error(ex, "CompleteMissionAsync failed {MissionId} {PlayerId}", missionId, playerId);
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class MissionService : IMissionService
{
private readonly IUnitOfWork _uow;
private readonly IRewardService _rewardService;
private readonly IRuleValidationService _ruleValidationService;
public MissionService(IUnitOfWork uow, IRewardService rewardService, IRuleValidationService ruleValidationService)
{
_uow = uow;
_rewardService = rewardService;
_ruleValidationService = ruleValidationService;
}
public async Task<Mission?> GetMissionByIdAsync(Guid missionId)
{
try { return await _uow.Missions.GetByIdAsync(missionId); }
catch (Exception ex) { Log.Error(ex, "GetMissionByIdAsync failed {MissionId}", missionId); throw; }
}
public async Task<IEnumerable<Mission>> GetMissionsByCategoryAsync(Guid categoryId)
{
try { return await _uow.Missions.Query(m => m.MissionCategoryId == categoryId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetMissionsByCategoryAsync failed {Category}", categoryId); throw; }
}
public async Task<IEnumerable<Mission>> GetAvailableMissionsForPlayerAsync(Guid playerId)
{
try
{
var missions = await _uow.Missions.Query().ToListAsync();
var result = new List<Mission>();
foreach (var m in missions)
{
if (await IsMissionAvailableForPlayerAsync(m.Id, playerId)) result.Add(m);
}
return result;
}
catch (Exception ex) { Log.Error(ex, "GetAvailableMissionsForPlayerAsync failed {Player}", playerId); throw; }
}
public async Task<IEnumerable<Mission>> GetChildMissionsAsync(Guid parentMissionId)
{
try { return await _uow.Missions.Query(m => m.ParentMissionId == parentMissionId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetChildMissionsAsync failed {ParentMission}", parentMissionId); throw; }
}
public async Task<Mission> CreateMissionAsync(Mission mission)
{
try
{
mission.Id = Guid.NewGuid();
await _uow.Missions.AddAsync(mission);
await _uow.SaveChangesAsync();
return mission;
}
catch (Exception ex) { Log.Error(ex, "CreateMissionAsync failed {Title}", mission.Title); throw; }
}
public async Task<Mission> UpdateMissionAsync(Mission mission)
{
try { _uow.Missions.Update(mission); await _uow.SaveChangesAsync(); return mission; }
catch (Exception ex) { Log.Error(ex, "UpdateMissionAsync failed {MissionId}", mission.Id); throw; }
}
public async Task<bool> DeleteMissionAsync(Guid missionId)
{
try { var m = await _uow.Missions.GetByIdAsync(missionId); if (m == null) return false; _uow.Missions.Remove(m); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteMissionAsync failed {MissionId}", missionId); throw; }
}
public async Task<bool> IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId)
{
try
{
var mission = await _uow.Missions.GetByIdAsync(missionId);
if (mission == null) return false;
// rule validation
if (!await _ruleValidationService.ValidateMissionRankRulesAsync(missionId, playerId)) return false;
// already completed? then not available
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId && pm.Completed != null).AnyAsync();
if (completed) return false;
// if parent mission required ensure parent completed
if (mission.ParentMissionId.HasValue)
{
var parentDone = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == mission.ParentMissionId && pm.Completed != null).AnyAsync();
if (!parentDone) return false;
}
return true;
}
catch (Exception ex) { Log.Error(ex, "IsMissionAvailableForPlayerAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<MissionCompletionResult> CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null)
{
try
{
if (!await IsMissionAvailableForPlayerAsync(missionId, playerId))
{
return new MissionCompletionResult { Success = false, Message = "Mission not available" };
}
var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found");
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
// snapshot skill levels before
var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync();
var beforeSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
// mark PlayerMission
var pm = await _uow.PlayerMissions.Query(x => x.PlayerId == playerId && x.MissionId == missionId).FirstOrDefaultAsync();
if (pm == null)
{
pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow };
await _uow.PlayerMissions.AddAsync(pm);
}
pm.Completed = DateTime.UtcNow;
pm.ProgressPercent = 100;
await _uow.SaveChangesAsync();
var prevExp = player.Experience;
var prevMana = player.Mana;
// distribute rewards (XP/Mana/Skills/Items)
await _rewardService.DistributeMissionRewardsAsync(missionId, playerId);
await _uow.SaveChangesAsync();
// build skill progress
var afterSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
var skillProgress = new List<SkillProgress>();
foreach (var r in skillRewards)
{
var before = beforeSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? 0;
var after = afterSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? before;
if (after != before)
{
var skill = await _uow.Skills.GetByIdAsync(r.SkillId);
skillProgress.Add(new SkillProgress { SkillId = r.SkillId, SkillTitle = skill?.Title ?? string.Empty, PreviousLevel = before, NewLevel = after });
}
}
return new MissionCompletionResult
{
Success = true,
Message = "Mission completed",
ExperienceGained = player.Experience - prevExp,
ManaGained = player.Mana - prevMana,
SkillsProgress = skillProgress,
UnlockedMissions = (await _uow.Missions.Query(m => m.ParentMissionId == missionId).Select(m => m.Id).ToListAsync())
};
}
catch (Exception ex)
{
Log.Error(ex, "CompleteMissionAsync failed {MissionId} {PlayerId}", missionId, playerId);
throw;
}
}
}

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Services.Models;
public class AnalyticsSummary
{
public int TotalUsers { get; set; }
public int TotalMissions { get; set; }
public int CompletedMissions { get; set; }
public int TotalStoreItems { get; set; }
public long TotalExperience { get; set; }
public DateTime GeneratedAtUtc { get; set; } = DateTime.UtcNow;
}
namespace LctMonolith.Services.Models;
public class AnalyticsSummary
{
public int TotalUsers { get; set; }
public int TotalMissions { get; set; }
public int CompletedMissions { get; set; }
public int TotalStoreItems { get; set; }
public long TotalExperience { get; set; }
public DateTime GeneratedAtUtc { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,9 +1,9 @@
namespace LctMonolith.Services.Models;
public class AuthRequest
{
public string Email { get; set; } = null!;
public string Password { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
namespace LctMonolith.Services.Models;
public class AuthRequest
{
public string Email { get; set; } = null!;
public string Password { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
}

View File

@@ -1,8 +1,8 @@
namespace LctMonolith.Services.Models;
public class CompetencyRewardModel
{
public Guid CompetencyId { get; set; }
public int LevelDelta { get; set; }
public int ProgressPointsDelta { get; set; }
}
namespace LctMonolith.Services.Models;
public class CompetencyRewardModel
{
public Guid CompetencyId { get; set; }
public int LevelDelta { get; set; }
public int ProgressPointsDelta { get; set; }
}

View File

@@ -1,15 +1,15 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Models;
public class CreateMissionModel
{
public string Title { get; set; } = null!;
public string? Description { get; set; }
public string? Branch { get; set; }
public MissionCategory Category { get; set; }
public Guid? MinRankId { get; set; }
public int ExperienceReward { get; set; }
public int ManaReward { get; set; }
public List<CompetencyRewardModel> CompetencyRewards { get; set; } = new();
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Models;
public class CreateMissionModel
{
public string Title { get; set; } = null!;
public string? Description { get; set; }
public string? Branch { get; set; }
public MissionCategory Category { get; set; }
public Guid? MinRankId { get; set; }
public int ExperienceReward { get; set; }
public int ManaReward { get; set; }
public List<CompetencyRewardModel> CompetencyRewards { get; set; } = new();
}

View File

@@ -1,23 +1,23 @@
namespace LctMonolith.Services.Models;
public class ProgressSnapshot
{
public int Experience { get; set; }
public int Mana { get; set; }
public Guid? CurrentRankId { get; set; }
public string? CurrentRankName { get; set; }
public Guid? NextRankId { get; set; }
public string? NextRankName { get; set; }
public int? RequiredExperienceForNextRank { get; set; }
public int? ExperienceRemaining => RequiredExperienceForNextRank.HasValue ? Math.Max(0, RequiredExperienceForNextRank.Value - Experience) : null;
public List<Guid> OutstandingMissionIds { get; set; } = new();
public List<OutstandingCompetency> OutstandingCompetencies { get; set; } = new();
}
public class OutstandingCompetency
{
public Guid CompetencyId { get; set; }
public string? CompetencyName { get; set; }
public int RequiredLevel { get; set; }
public int CurrentLevel { get; set; }
}
namespace LctMonolith.Services.Models;
public class ProgressSnapshot
{
public int Experience { get; set; }
public int Mana { get; set; }
public Guid? CurrentRankId { get; set; }
public string? CurrentRankName { get; set; }
public Guid? NextRankId { get; set; }
public string? NextRankName { get; set; }
public int? RequiredExperienceForNextRank { get; set; }
public int? ExperienceRemaining => RequiredExperienceForNextRank.HasValue ? Math.Max(0, RequiredExperienceForNextRank.Value - Experience) : null;
public List<Guid> OutstandingMissionIds { get; set; } = new();
public List<OutstandingCompetency> OutstandingCompetencies { get; set; } = new();
}
public class OutstandingCompetency
{
public Guid CompetencyId { get; set; }
public string? CompetencyName { get; set; }
public int RequiredLevel { get; set; }
public int CurrentLevel { get; set; }
}

View File

@@ -1,7 +1,7 @@
namespace LctMonolith.Services.Models;
public class PurchaseRequest
{
public Guid ItemId { get; set; }
public int Quantity { get; set; } = 1;
}
namespace LctMonolith.Services.Models;
public class PurchaseRequest
{
public Guid ItemId { get; set; }
public int Quantity { get; set; } = 1;
}

View File

@@ -1,6 +1,6 @@
namespace LctMonolith.Services.Models;
public class RefreshRequest
{
public string RefreshToken { get; set; } = null!;
}
namespace LctMonolith.Services.Models;
public class RefreshRequest
{
public string RefreshToken { get; set; } = null!;
}

View File

@@ -1,6 +1,6 @@
namespace LctMonolith.Services.Models;
public class RevokeRequest
{
public string RefreshToken { get; set; } = null!;
}
namespace LctMonolith.Services.Models;
public class RevokeRequest
{
public string RefreshToken { get; set; } = null!;
}

View File

@@ -1,3 +1,3 @@
namespace LctMonolith.Services.Models;
public record TokenPair(string AccessToken, DateTime AccessTokenExpiresAt, string RefreshToken, DateTime RefreshTokenExpiresAt);
namespace LctMonolith.Services.Models;
public record TokenPair(string AccessToken, DateTime AccessTokenExpiresAt, string RefreshToken, DateTime RefreshTokenExpiresAt);

View File

@@ -1,68 +1,68 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// In-app user notifications CRUD / read-state operations.
/// </summary>
public class NotificationService : INotificationService
{
private readonly IUnitOfWork _uow;
public NotificationService(IUnitOfWork uow)
{
_uow = uow;
}
public async Task<Notification> CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default)
{
try
{
if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Required", nameof(type));
if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Required", nameof(title));
if (string.IsNullOrWhiteSpace(message)) throw new ArgumentException("Required", nameof(message));
var n = new Notification
{
UserId = userId,
Type = type.Trim(),
Title = title.Trim(),
Message = message.Trim(),
CreatedAt = DateTime.UtcNow,
IsRead = false
};
await _uow.Notifications.AddAsync(n, ct);
await _uow.SaveChangesAsync(ct);
return n;
}
catch (Exception ex) { Log.Error(ex, "Notification CreateAsync failed {UserId}", userId); throw; }
}
public async Task<IEnumerable<Notification>> GetUnreadAsync(Guid userId, CancellationToken ct = default)
{
try { return await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead, q => q.OrderByDescending(x => x.CreatedAt)).Take(100).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetUnreadAsync failed {UserId}", userId); throw; }
}
public async Task<IEnumerable<Notification>> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default)
{
try { if (take <= 0) take = 1; if (take > 500) take = 500; return await _uow.Notifications.Query(n => n.UserId == userId, q => q.OrderByDescending(x => x.CreatedAt)).Take(take).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetAllAsync failed {UserId}", userId); throw; }
}
public async Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default)
{
try { var notif = await _uow.Notifications.Query(n => n.Id == notificationId && n.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Notification not found"); if (!notif.IsRead) { notif.IsRead = true; notif.ReadAt = DateTime.UtcNow; await _uow.SaveChangesAsync(ct); } }
catch (Exception ex) { Log.Error(ex, "MarkReadAsync failed {NotificationId}", notificationId); throw; }
}
public async Task<int> MarkAllReadAsync(Guid userId, CancellationToken ct = default)
{
try { var unread = await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead).ToListAsync(ct); if (unread.Count == 0) return 0; var now = DateTime.UtcNow; foreach (var n in unread) { n.IsRead = true; n.ReadAt = now; } await _uow.SaveChangesAsync(ct); return unread.Count; }
catch (Exception ex) { Log.Error(ex, "MarkAllReadAsync failed {UserId}", userId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// In-app user notifications CRUD / read-state operations.
/// </summary>
public class NotificationService : INotificationService
{
private readonly IUnitOfWork _uow;
public NotificationService(IUnitOfWork uow)
{
_uow = uow;
}
public async Task<Notification> CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default)
{
try
{
if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Required", nameof(type));
if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Required", nameof(title));
if (string.IsNullOrWhiteSpace(message)) throw new ArgumentException("Required", nameof(message));
var n = new Notification
{
UserId = userId,
Type = type.Trim(),
Title = title.Trim(),
Message = message.Trim(),
CreatedAt = DateTime.UtcNow,
IsRead = false
};
await _uow.Notifications.AddAsync(n, ct);
await _uow.SaveChangesAsync(ct);
return n;
}
catch (Exception ex) { Log.Error(ex, "Notification CreateAsync failed {UserId}", userId); throw; }
}
public async Task<IEnumerable<Notification>> GetUnreadAsync(Guid userId, CancellationToken ct = default)
{
try { return await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead, q => q.OrderByDescending(x => x.CreatedAt)).Take(100).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetUnreadAsync failed {UserId}", userId); throw; }
}
public async Task<IEnumerable<Notification>> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default)
{
try { if (take <= 0) take = 1; if (take > 500) take = 500; return await _uow.Notifications.Query(n => n.UserId == userId, q => q.OrderByDescending(x => x.CreatedAt)).Take(take).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetAllAsync failed {UserId}", userId); throw; }
}
public async Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default)
{
try { var notif = await _uow.Notifications.Query(n => n.Id == notificationId && n.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Notification not found"); if (!notif.IsRead) { notif.IsRead = true; notif.ReadAt = DateTime.UtcNow; await _uow.SaveChangesAsync(ct); } }
catch (Exception ex) { Log.Error(ex, "MarkReadAsync failed {NotificationId}", notificationId); throw; }
}
public async Task<int> MarkAllReadAsync(Guid userId, CancellationToken ct = default)
{
try { var unread = await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead).ToListAsync(ct); if (unread.Count == 0) return 0; var now = DateTime.UtcNow; foreach (var n in unread) { n.IsRead = true; n.ReadAt = now; } await _uow.SaveChangesAsync(ct); return unread.Count; }
catch (Exception ex) { Log.Error(ex, "MarkAllReadAsync failed {UserId}", userId); throw; }
}
}

View File

@@ -1,153 +1,153 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class PlayerService : IPlayerService
{
private readonly IUnitOfWork _uow;
public PlayerService(IUnitOfWork uow) => _uow = uow;
public async Task<Player?> GetPlayerByUserIdAsync(string userId)
{
try
{
if (!Guid.TryParse(userId, out var uid)) return null;
return await _uow.Players.Query(p => p.UserId == uid, null, p => p.Rank).FirstOrDefaultAsync();
}
catch (Exception ex)
{
Log.Error(ex, "GetPlayerByUserIdAsync failed {UserId}", userId);
throw;
}
}
public async Task<Player> CreatePlayerAsync(string userId, string username)
{
try
{
if (!Guid.TryParse(userId, out var uid)) throw new ArgumentException("Invalid user id", nameof(userId));
var existing = await GetPlayerByUserIdAsync(userId);
if (existing != null) return existing;
// pick lowest exp rank
var baseRank = await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).FirstAsync();
var player = new Player
{
Id = Guid.NewGuid(),
UserId = uid,
RankId = baseRank.Id,
Experience = 0,
Mana = 0
};
await _uow.Players.AddAsync(player);
await _uow.SaveChangesAsync();
Log.Information("Created player {PlayerId} for user {UserId}", player.Id, userId);
return player;
}
catch (Exception ex)
{
Log.Error(ex, "CreatePlayerAsync failed {UserId}", userId);
throw;
}
}
public async Task<Player> UpdatePlayerRankAsync(Guid playerId, Guid newRankId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var rank = await _uow.Ranks.GetByIdAsync(newRankId) ?? throw new KeyNotFoundException("Rank not found");
player.RankId = rank.Id;
await _uow.SaveChangesAsync();
return player;
}
catch (Exception ex)
{
Log.Error(ex, "UpdatePlayerRankAsync failed {PlayerId} -> {RankId}", playerId, newRankId);
throw;
}
}
public async Task<Player> AddPlayerExperienceAsync(Guid playerId, int experience)
{
try
{
if (experience == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException();
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Experience += experience;
await _uow.SaveChangesAsync();
await AutoRankUpAsync(player);
return player;
}
catch (Exception ex)
{
Log.Error(ex, "AddPlayerExperienceAsync failed {PlayerId}", playerId);
throw;
}
}
private async Task AutoRankUpAsync(Player player)
{
// find highest rank whose ExpNeeded <= player's experience
var target = await _uow.Ranks.Query(r => r.ExpNeeded <= player.Experience)
.OrderByDescending(r => r.ExpNeeded)
.FirstOrDefaultAsync();
if (target != null && target.Id != player.RankId)
{
player.RankId = target.Id;
await _uow.SaveChangesAsync();
Log.Information("Player {Player} advanced to rank {Rank}", player.Id, target.Title);
}
}
public async Task<Player> AddPlayerManaAsync(Guid playerId, int mana)
{
try
{
if (mana == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException();
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Mana += mana;
await _uow.SaveChangesAsync();
return player;
}
catch (Exception ex)
{
Log.Error(ex, "AddPlayerManaAsync failed {PlayerId}", playerId);
throw;
}
}
public async Task<IEnumerable<Player>> GetTopPlayersAsync(int topCount, TimeSpan timeFrame)
{
try
{
// Simple ordering by experience (timeFrame ignored due to no timestamp on Player)
return await _uow.Players.Query()
.OrderByDescending(p => p.Experience)
.Take(topCount)
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "GetTopPlayersAsync failed");
throw;
}
}
public async Task<Player> GetPlayerWithProgressAsync(Guid playerId)
{
try
{
return await _uow.Players.Query(p => p.Id == playerId, null, p => p.Rank)
.FirstOrDefaultAsync() ?? throw new KeyNotFoundException("Player not found");
}
catch (Exception ex)
{
Log.Error(ex, "GetPlayerWithProgressAsync failed {PlayerId}", playerId);
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class PlayerService : IPlayerService
{
private readonly IUnitOfWork _uow;
public PlayerService(IUnitOfWork uow) => _uow = uow;
public async Task<Player?> GetPlayerByUserIdAsync(string userId)
{
try
{
if (!Guid.TryParse(userId, out var uid)) return null;
return await _uow.Players.Query(p => p.UserId == uid, null, p => p.Rank).FirstOrDefaultAsync();
}
catch (Exception ex)
{
Log.Error(ex, "GetPlayerByUserIdAsync failed {UserId}", userId);
throw;
}
}
public async Task<Player> CreatePlayerAsync(string userId, string username)
{
try
{
if (!Guid.TryParse(userId, out var uid)) throw new ArgumentException("Invalid user id", nameof(userId));
var existing = await GetPlayerByUserIdAsync(userId);
if (existing != null) return existing;
// pick lowest exp rank
var baseRank = await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).FirstAsync();
var player = new Player
{
Id = Guid.NewGuid(),
UserId = uid,
RankId = baseRank.Id,
Experience = 0,
Mana = 0
};
await _uow.Players.AddAsync(player);
await _uow.SaveChangesAsync();
Log.Information("Created player {PlayerId} for user {UserId}", player.Id, userId);
return player;
}
catch (Exception ex)
{
Log.Error(ex, "CreatePlayerAsync failed {UserId}", userId);
throw;
}
}
public async Task<Player> UpdatePlayerRankAsync(Guid playerId, Guid newRankId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var rank = await _uow.Ranks.GetByIdAsync(newRankId) ?? throw new KeyNotFoundException("Rank not found");
player.RankId = rank.Id;
await _uow.SaveChangesAsync();
return player;
}
catch (Exception ex)
{
Log.Error(ex, "UpdatePlayerRankAsync failed {PlayerId} -> {RankId}", playerId, newRankId);
throw;
}
}
public async Task<Player> AddPlayerExperienceAsync(Guid playerId, int experience)
{
try
{
if (experience == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException();
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Experience += experience;
await _uow.SaveChangesAsync();
await AutoRankUpAsync(player);
return player;
}
catch (Exception ex)
{
Log.Error(ex, "AddPlayerExperienceAsync failed {PlayerId}", playerId);
throw;
}
}
private async Task AutoRankUpAsync(Player player)
{
// find highest rank whose ExpNeeded <= player's experience
var target = await _uow.Ranks.Query(r => r.ExpNeeded <= player.Experience)
.OrderByDescending(r => r.ExpNeeded)
.FirstOrDefaultAsync();
if (target != null && target.Id != player.RankId)
{
player.RankId = target.Id;
await _uow.SaveChangesAsync();
Log.Information("Player {Player} advanced to rank {Rank}", player.Id, target.Title);
}
}
public async Task<Player> AddPlayerManaAsync(Guid playerId, int mana)
{
try
{
if (mana == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException();
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Mana += mana;
await _uow.SaveChangesAsync();
return player;
}
catch (Exception ex)
{
Log.Error(ex, "AddPlayerManaAsync failed {PlayerId}", playerId);
throw;
}
}
public async Task<IEnumerable<Player>> GetTopPlayersAsync(int topCount, TimeSpan timeFrame)
{
try
{
// Simple ordering by experience (timeFrame ignored due to no timestamp on Player)
return await _uow.Players.Query()
.OrderByDescending(p => p.Experience)
.Take(topCount)
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "GetTopPlayersAsync failed");
throw;
}
}
public async Task<Player> GetPlayerWithProgressAsync(Guid playerId)
{
try
{
return await _uow.Players.Query(p => p.Id == playerId, null, p => p.Rank)
.FirstOrDefaultAsync() ?? throw new KeyNotFoundException("Player not found");
}
catch (Exception ex)
{
Log.Error(ex, "GetPlayerWithProgressAsync failed {PlayerId}", playerId);
throw;
}
}
}

View File

@@ -1,121 +1,121 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Serilog;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace LctMonolith.Services;
public class ProfileService : IProfileService
{
private readonly IUnitOfWork _uow;
private readonly IFileStorageService _storage;
public ProfileService(IUnitOfWork uow, IFileStorageService storage)
{
_uow = uow;
_storage = storage;
}
public async Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default)
{
try
{
return await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "Profile get failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null)
{
profile = new Profile
{
Id = Guid.NewGuid(),
UserId = userId,
FirstName = firstName,
LastName = lastName,
BirthDate = birthDate,
About = about,
Location = location
};
await _uow.Profiles.AddAsync(profile, ct);
}
else
{
profile.FirstName = firstName;
profile.LastName = lastName;
profile.BirthDate = birthDate;
profile.About = about;
profile.Location = location;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
}
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Profile upsert failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ??
await UpsertAsync(userId, null, null, null, null, null, ct);
// Delete old if exists
if (!string.IsNullOrWhiteSpace(profile.AvatarS3Key))
{
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
}
var key = await _storage.UploadAsync(fileStream, contentType, $"avatars/{userId}", ct);
var url = await _storage.GetPresignedUrlAsync(key, TimeSpan.FromHours(6), ct);
profile.AvatarS3Key = key;
profile.AvatarUrl = url;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Avatar update failed {UserId}", userId);
throw;
}
}
public async Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null || string.IsNullOrWhiteSpace(profile.AvatarS3Key)) return false;
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
profile.AvatarS3Key = null;
profile.AvatarUrl = null;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Delete avatar failed {UserId}", userId);
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Serilog;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace LctMonolith.Services;
public class ProfileService : IProfileService
{
private readonly IUnitOfWork _uow;
private readonly IFileStorageService _storage;
public ProfileService(IUnitOfWork uow, IFileStorageService storage)
{
_uow = uow;
_storage = storage;
}
public async Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default)
{
try
{
return await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "Profile get failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null)
{
profile = new Profile
{
Id = Guid.NewGuid(),
UserId = userId,
FirstName = firstName,
LastName = lastName,
BirthDate = birthDate,
About = about,
Location = location
};
await _uow.Profiles.AddAsync(profile, ct);
}
else
{
profile.FirstName = firstName;
profile.LastName = lastName;
profile.BirthDate = birthDate;
profile.About = about;
profile.Location = location;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
}
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Profile upsert failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ??
await UpsertAsync(userId, null, null, null, null, null, ct);
// Delete old if exists
if (!string.IsNullOrWhiteSpace(profile.AvatarS3Key))
{
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
}
var key = await _storage.UploadAsync(fileStream, contentType, $"avatars/{userId}", ct);
var url = await _storage.GetPresignedUrlAsync(key, TimeSpan.FromHours(6), ct);
profile.AvatarS3Key = key;
profile.AvatarUrl = url;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Avatar update failed {UserId}", userId);
throw;
}
}
public async Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null || string.IsNullOrWhiteSpace(profile.AvatarS3Key)) return false;
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
profile.AvatarS3Key = null;
profile.AvatarUrl = null;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Delete avatar failed {UserId}", userId);
throw;
}
}
}

View File

@@ -1,103 +1,103 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class ProgressTrackingService : IProgressTrackingService
{
private readonly IUnitOfWork _uow;
private readonly IMissionService _missionService;
public ProgressTrackingService(IUnitOfWork uow, IMissionService missionService)
{
_uow = uow;
_missionService = missionService;
}
public async Task<PlayerMission> StartMissionAsync(Guid missionId, Guid playerId)
{
try
{
var existing = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync();
if (existing != null) return existing;
var pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow, ProgressPercent = 0 };
await _uow.PlayerMissions.AddAsync(pm);
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "StartMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<PlayerMission> UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null)
{
try
{
if (progressPercentage is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(progressPercentage));
var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found");
if (pm.Completed != null) return pm;
pm.ProgressPercent = progressPercentage;
if (progressPercentage == 100 && pm.Completed == null)
{
// Complete mission through mission service to allocate rewards, etc.
await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId);
}
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "UpdateMissionProgressAsync failed {PlayerMissionId}", playerMissionId); throw; }
}
public async Task<PlayerMission> CompleteMissionAsync(Guid playerMissionId, object? proof = null)
{
try
{
var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found");
if (pm.Completed != null) return pm;
await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId, proof);
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "CompleteMissionAsync (progress) failed {PlayerMissionId}", playerMissionId); throw; }
}
public async Task<IEnumerable<PlayerMission>> GetPlayerMissionsAsync(Guid playerId)
{
try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerMissionsAsync failed {PlayerId}", playerId); throw; }
}
public async Task<PlayerMission?> GetPlayerMissionAsync(Guid playerId, Guid missionId)
{
try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<PlayerProgress> GetPlayerOverallProgressAsync(Guid playerId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var missions = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync();
var completed = missions.Count(m => m.Completed != null);
var totalMissions = await _uow.Missions.Query().CountAsync();
var skillLevels = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill)
.ToDictionaryAsync(ps => ps.Skill.Title, ps => ps.Score);
return new PlayerProgress
{
PlayerId = playerId,
PlayerName = playerId.ToString(),
CurrentRank = await _uow.Ranks.GetByIdAsync(player.RankId),
TotalExperience = player.Experience,
TotalMana = player.Mana,
CompletedMissions = completed,
TotalAvailableMissions = totalMissions,
SkillLevels = skillLevels
};
}
catch (Exception ex) { Log.Error(ex, "GetPlayerOverallProgressAsync failed {PlayerId}", playerId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class ProgressTrackingService : IProgressTrackingService
{
private readonly IUnitOfWork _uow;
private readonly IMissionService _missionService;
public ProgressTrackingService(IUnitOfWork uow, IMissionService missionService)
{
_uow = uow;
_missionService = missionService;
}
public async Task<PlayerMission> StartMissionAsync(Guid missionId, Guid playerId)
{
try
{
var existing = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync();
if (existing != null) return existing;
var pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow, ProgressPercent = 0 };
await _uow.PlayerMissions.AddAsync(pm);
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "StartMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<PlayerMission> UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null)
{
try
{
if (progressPercentage is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(progressPercentage));
var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found");
if (pm.Completed != null) return pm;
pm.ProgressPercent = progressPercentage;
if (progressPercentage == 100 && pm.Completed == null)
{
// Complete mission through mission service to allocate rewards, etc.
await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId);
}
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "UpdateMissionProgressAsync failed {PlayerMissionId}", playerMissionId); throw; }
}
public async Task<PlayerMission> CompleteMissionAsync(Guid playerMissionId, object? proof = null)
{
try
{
var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found");
if (pm.Completed != null) return pm;
await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId, proof);
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "CompleteMissionAsync (progress) failed {PlayerMissionId}", playerMissionId); throw; }
}
public async Task<IEnumerable<PlayerMission>> GetPlayerMissionsAsync(Guid playerId)
{
try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerMissionsAsync failed {PlayerId}", playerId); throw; }
}
public async Task<PlayerMission?> GetPlayerMissionAsync(Guid playerId, Guid missionId)
{
try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<PlayerProgress> GetPlayerOverallProgressAsync(Guid playerId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var missions = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync();
var completed = missions.Count(m => m.Completed != null);
var totalMissions = await _uow.Missions.Query().CountAsync();
var skillLevels = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill)
.ToDictionaryAsync(ps => ps.Skill.Title, ps => ps.Score);
return new PlayerProgress
{
PlayerId = playerId,
PlayerName = playerId.ToString(),
CurrentRank = await _uow.Ranks.GetByIdAsync(player.RankId),
TotalExperience = player.Experience,
TotalMana = player.Mana,
CompletedMissions = completed,
TotalAvailableMissions = totalMissions,
SkillLevels = skillLevels
};
}
catch (Exception ex) { Log.Error(ex, "GetPlayerOverallProgressAsync failed {PlayerId}", playerId); throw; }
}
}

View File

@@ -1,75 +1,75 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class RankService : IRankService
{
private readonly IUnitOfWork _uow;
public RankService(IUnitOfWork uow) => _uow = uow;
public async Task<Rank?> GetRankByIdAsync(Guid rankId)
{
try { return await _uow.Ranks.GetByIdAsync(rankId); }
catch (Exception ex) { Log.Error(ex, "GetRankByIdAsync failed {RankId}", rankId); throw; }
}
public async Task<Rank?> GetRankByTitleAsync(string title)
{
try { return await _uow.Ranks.Query(r => r.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetRankByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<Rank>> GetAllRanksAsync()
{
try { return await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllRanksAsync failed"); throw; }
}
public async Task<Rank> CreateRankAsync(Rank rank)
{
try { rank.Id = Guid.NewGuid(); await _uow.Ranks.AddAsync(rank); await _uow.SaveChangesAsync(); return rank; }
catch (Exception ex) { Log.Error(ex, "CreateRankAsync failed {Title}", rank.Title); throw; }
}
public async Task<Rank> UpdateRankAsync(Rank rank)
{
try { _uow.Ranks.Update(rank); await _uow.SaveChangesAsync(); return rank; }
catch (Exception ex) { Log.Error(ex, "UpdateRankAsync failed {RankId}", rank.Id); throw; }
}
public async Task<bool> DeleteRankAsync(Guid rankId)
{
try { var r = await _uow.Ranks.GetByIdAsync(rankId); if (r == null) return false; _uow.Ranks.Remove(r); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteRankAsync failed {RankId}", rankId); throw; }
}
public async Task<bool> CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId)
{
try {
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var rank = await _uow.Ranks.GetByIdAsync(rankId) ?? throw new KeyNotFoundException("Rank not found");
if (player.Experience < rank.ExpNeeded) return false;
var missionReqs = await _uow.RankMissionRules.Query(rmr => rmr.RankId == rankId).Select(r => r.MissionId).ToListAsync();
if (missionReqs.Count > 0)
{
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync();
if (missionReqs.Except(completed).Any()) return false;
}
var skillReqs = await _uow.RankSkillRules.Query(rsr => rsr.RankId == rankId).ToListAsync();
if (skillReqs.Count > 0)
{
var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
foreach (var req in skillReqs)
{
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
if (ps == null || ps.Score < req.Min) return false;
}
}
return true; }
catch (Exception ex) { Log.Error(ex, "CanPlayerAdvanceToRankAsync failed {PlayerId}->{RankId}", playerId, rankId); throw; }
}
public async Task<Rank?> GetNextRankAsync(Guid currentRankId)
{
try {
var current = await _uow.Ranks.GetByIdAsync(currentRankId); if (current == null) return null; return await _uow.Ranks.Query(r => r.ExpNeeded > current.ExpNeeded).OrderBy(r => r.ExpNeeded).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetNextRankAsync failed {RankId}", currentRankId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class RankService : IRankService
{
private readonly IUnitOfWork _uow;
public RankService(IUnitOfWork uow) => _uow = uow;
public async Task<Rank?> GetRankByIdAsync(Guid rankId)
{
try { return await _uow.Ranks.GetByIdAsync(rankId); }
catch (Exception ex) { Log.Error(ex, "GetRankByIdAsync failed {RankId}", rankId); throw; }
}
public async Task<Rank?> GetRankByTitleAsync(string title)
{
try { return await _uow.Ranks.Query(r => r.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetRankByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<Rank>> GetAllRanksAsync()
{
try { return await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllRanksAsync failed"); throw; }
}
public async Task<Rank> CreateRankAsync(Rank rank)
{
try { rank.Id = Guid.NewGuid(); await _uow.Ranks.AddAsync(rank); await _uow.SaveChangesAsync(); return rank; }
catch (Exception ex) { Log.Error(ex, "CreateRankAsync failed {Title}", rank.Title); throw; }
}
public async Task<Rank> UpdateRankAsync(Rank rank)
{
try { _uow.Ranks.Update(rank); await _uow.SaveChangesAsync(); return rank; }
catch (Exception ex) { Log.Error(ex, "UpdateRankAsync failed {RankId}", rank.Id); throw; }
}
public async Task<bool> DeleteRankAsync(Guid rankId)
{
try { var r = await _uow.Ranks.GetByIdAsync(rankId); if (r == null) return false; _uow.Ranks.Remove(r); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteRankAsync failed {RankId}", rankId); throw; }
}
public async Task<bool> CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId)
{
try {
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var rank = await _uow.Ranks.GetByIdAsync(rankId) ?? throw new KeyNotFoundException("Rank not found");
if (player.Experience < rank.ExpNeeded) return false;
var missionReqs = await _uow.RankMissionRules.Query(rmr => rmr.RankId == rankId).Select(r => r.MissionId).ToListAsync();
if (missionReqs.Count > 0)
{
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync();
if (missionReqs.Except(completed).Any()) return false;
}
var skillReqs = await _uow.RankSkillRules.Query(rsr => rsr.RankId == rankId).ToListAsync();
if (skillReqs.Count > 0)
{
var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
foreach (var req in skillReqs)
{
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
if (ps == null || ps.Score < req.Min) return false;
}
}
return true; }
catch (Exception ex) { Log.Error(ex, "CanPlayerAdvanceToRankAsync failed {PlayerId}->{RankId}", playerId, rankId); throw; }
}
public async Task<Rank?> GetNextRankAsync(Guid currentRankId)
{
try {
var current = await _uow.Ranks.GetByIdAsync(currentRankId); if (current == null) return null; return await _uow.Ranks.Query(r => r.ExpNeeded > current.ExpNeeded).OrderBy(r => r.ExpNeeded).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetNextRankAsync failed {RankId}", currentRankId); throw; }
}
}

Some files were not shown because too many files have changed in this diff Show More