first commit

This commit is contained in:
2026-07-09 10:02:08 +05:30
commit 438ab2cdbd
27 changed files with 2164 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
using System.Security.Claims;
namespace ERPCore.Infra.Auth;
/// <summary>
/// Resolves the audit actor from the current HTTP request's principal
/// (token `sub` / NameIdentifier). Falls back to <see cref="SystemActor"/>
/// when the request is unauthenticated (e.g. health checks, seeding).
/// </summary>
public sealed class CurrentUser : ICurrentUser
{
public const string SystemActor = "system";
private readonly IHttpContextAccessor _accessor;
public CurrentUser(IHttpContextAccessor accessor) => _accessor = accessor;
public bool IsAuthenticated =>
_accessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
public string UserId
{
get
{
var user = _accessor.HttpContext?.User;
var sub = user?.FindFirstValue(ClaimTypes.NameIdentifier)
?? user?.FindFirstValue("sub");
return string.IsNullOrWhiteSpace(sub) ? SystemActor : sub;
}
}
}
@@ -0,0 +1,15 @@
namespace ERPCore.Infra.Auth;
/// <summary>
/// Abstraction over the authenticated principal, used purely as the audit actor
/// (RBAC is deferred for Phase 1). The actor is derived from the token's `sub`
/// claim — never from the request body (see 00-CORE §4).
/// </summary>
public interface ICurrentUser
{
/// <summary>The audit actor identity (token `sub`), or "system" when unauthenticated.</summary>
string UserId { get; }
/// <summary>True when the request carries an authenticated principal.</summary>
bool IsAuthenticated { get; }
}
@@ -0,0 +1,39 @@
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
namespace ERPCore.Infra.Auth;
/// <summary>
/// JWT bearer wiring. Authentication only — RBAC/authorization policies are
/// deferred for Phase 1; the validated principal exists solely so that
/// <see cref="ICurrentUser"/> can stamp the audit actor.
/// </summary>
public static class JwtAuthExtensions
{
public static IServiceCollection AddErpJwtAuth(this IServiceCollection services, IConfiguration config)
{
var issuer = config["Jwt:Issuer"];
var audience = config["Jwt:Audience"];
var signingKey = config["Jwt:SigningKey"] ?? string.Empty;
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateAudience = true,
ValidAudience = audience,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
ClockSkew = TimeSpan.FromSeconds(30)
};
});
services.AddAuthorization();
return services;
}
}
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
namespace ERPCore.Infra.Persistence;
/// <summary>
/// EF Core context for the ERP database. The 38 Phase 1 entities and their
/// <see cref="IEntityTypeConfiguration{TEntity}"/> configurations are added under
/// Domain/Entities and Infra/Persistence/Configurations as they are implemented.
/// The authoritative schema lives in docs/10-BACKEND-PHASE1.md — do not invent it here.
/// </summary>
public class ErpDbContext : DbContext
{
public ErpDbContext(DbContextOptions<ErpDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Pick up every IEntityTypeConfiguration in this assembly
// (Infra/Persistence/Configurations/*).
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ErpDbContext).Assembly);
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace ERPCore.Infra.UoW;
/// <summary>
/// Transaction boundary for stock-affecting operations (00-CORE §4). Every
/// GRN confirm, transfer dispatch/receive, adjustment, return and count post
/// runs inside a single UoW transaction so FIFO layer consumption and ledger
/// writes commit or roll back together.
/// </summary>
public interface IUnitOfWork
{
/// <summary>Persist tracked changes without an explicit transaction scope.</summary>
Task<int> SaveChangesAsync(CancellationToken ct = default);
/// <summary>Run <paramref name="action"/> inside a transaction, saving and committing on success.</summary>
Task ExecuteInTransactionAsync(Func<CancellationToken, Task> action, CancellationToken ct = default);
/// <summary>Transactional variant that returns a value from <paramref name="action"/>.</summary>
Task<T> ExecuteInTransactionAsync<T>(Func<CancellationToken, Task<T>> action, CancellationToken ct = default);
}
+47
View File
@@ -0,0 +1,47 @@
using ERPCore.Infra.Persistence;
using Microsoft.EntityFrameworkCore;
namespace ERPCore.Infra.UoW;
/// <summary>
/// EF Core-backed unit of work. Uses the provider execution strategy so the
/// transactional path is safe under connection resiliency / retries.
/// </summary>
public sealed class UnitOfWork : IUnitOfWork
{
private readonly ErpDbContext _db;
public UnitOfWork(ErpDbContext db) => _db = db;
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
public async Task ExecuteInTransactionAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
{
await ExecuteInTransactionAsync<object?>(async token =>
{
await action(token);
return null;
}, ct);
}
public async Task<T> ExecuteInTransactionAsync<T>(Func<CancellationToken, Task<T>> action, CancellationToken ct = default)
{
var strategy = _db.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);
try
{
var result = await action(ct);
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return result;
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
});
}
}