diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..38bece4
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,25 @@
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/.idea
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
\ No newline at end of file
diff --git a/LCT2025.sln b/LCT2025.sln
index 2bfd4ed..ee320c6 100644
--- a/LCT2025.sln
+++ b/LCT2025.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StoreService", "StoreService\StoreService.csproj", "{5DC91A89-84D9-4C5E-9539-43757A2474D5}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LctMonolith", "LctMonolith\LctMonolith.csproj", "{EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -8,9 +8,9 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {5DC91A89-84D9-4C5E-9539-43757A2474D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5DC91A89-84D9-4C5E-9539-43757A2474D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5DC91A89-84D9-4C5E-9539-43757A2474D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5DC91A89-84D9-4C5E-9539-43757A2474D5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/LCT2025.sln.DotSettings.user b/LCT2025.sln.DotSettings.user
new file mode 100644
index 0000000..6652580
--- /dev/null
+++ b/LCT2025.sln.DotSettings.user
@@ -0,0 +1,2 @@
+
+ ForceIncluded
\ No newline at end of file
diff --git a/LctMonolith/Application/Middleware/ErrorHandlingMiddleware.cs b/LctMonolith/Application/Middleware/ErrorHandlingMiddleware.cs
new file mode 100644
index 0000000..fa30be0
--- /dev/null
+++ b/LctMonolith/Application/Middleware/ErrorHandlingMiddleware.cs
@@ -0,0 +1,46 @@
+using System.Net;
+using System.Text.Json;
+using Serilog;
+
+namespace LctMonolith.Application.Middleware;
+
+///
+/// Global error handling middleware capturing unhandled exceptions and converting to standardized JSON response.
+///
+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
+{
+ /// Adds global error handling middleware.
+ public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder app) => app.UseMiddleware();
+}
diff --git a/LctMonolith/Application/Options/JwtOptions.cs b/LctMonolith/Application/Options/JwtOptions.cs
new file mode 100644
index 0000000..a44b98c
--- /dev/null
+++ b/LctMonolith/Application/Options/JwtOptions.cs
@@ -0,0 +1,14 @@
+namespace LctMonolith.Application.Options;
+
+///
+/// JWT issuing configuration loaded from appsettings (section Jwt).
+///
+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;
+}
+
diff --git a/LctMonolith/Controllers/AuthController.cs b/LctMonolith/Controllers/AuthController.cs
new file mode 100644
index 0000000..b9acfef
--- /dev/null
+++ b/LctMonolith/Controllers/AuthController.cs
@@ -0,0 +1,83 @@
+using System.Security.Claims;
+using LctMonolith.Domain.Entities;
+using LctMonolith.Services;
+using LctMonolith.Services.Models;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LctMonolith.Controllers;
+
+///
+/// Authentication endpoints (mocked local identity + JWT issuing).
+///
+[ApiController]
+[Route("api/auth")]
+public class AuthController : ControllerBase
+{
+ private readonly UserManager _userManager;
+ private readonly SignInManager _signInManager;
+ private readonly ITokenService _tokenService;
+
+ public AuthController(UserManager userManager, SignInManager signInManager, ITokenService tokenService)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ _tokenService = tokenService;
+ }
+
+ /// Registers a new user (simplified).
+ [HttpPost("register")]
+ [AllowAnonymous]
+ public async Task> 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);
+ }
+
+ /// Login with email + password.
+ [HttpPost("login")]
+ [AllowAnonymous]
+ public async Task> 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);
+ }
+
+ /// Refresh access token by refresh token.
+ [HttpPost("refresh")]
+ [AllowAnonymous]
+ public async Task> Refresh(RefreshRequest req, CancellationToken ct)
+ {
+ var pair = await _tokenService.RefreshAsync(req.RefreshToken, ct);
+ return Ok(pair);
+ }
+
+ /// Revoke refresh token (logout).
+ [HttpPost("revoke")]
+ [Authorize]
+ public async Task Revoke(RevokeRequest req, CancellationToken ct)
+ {
+ await _tokenService.RevokeAsync(req.RefreshToken, ct);
+ return NoContent();
+ }
+
+ /// Returns current user id (debug).
+ [HttpGet("me")]
+ [Authorize]
+ public ActionResult