first commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user