Added components, authentication and authorization

This commit is contained in:
2026-05-04 16:53:19 +05:00
parent 493cd71d17
commit fb1cb8e834
37 changed files with 3545 additions and 21 deletions
+83
View File
@@ -0,0 +1,83 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
namespace Htmx.ApiDemo.Data;
/// <summary>
/// AOT-safe authentication service backed by MongoDB.
/// No EF Core, no LINQ-to-SQL, no RelationalModel fully NativeAOT safe.
/// IPasswordHasher is pure PBKDF2 crypto with no dynamic IL.
/// </summary>
public sealed class AuthService(
MongoDbService mongo,
IPasswordHasher<AppUser> passwordHasher,
IHttpContextAccessor httpContextAccessor)
{
public async Task<(bool Success, string? Error)> RegisterAsync(
string email, string password, string? displayName, CancellationToken ct = default)
{
var normalized = email.ToUpperInvariant();
if (await mongo.EmailExistsAsync(normalized, ct))
return (false, "That email address is already registered.");
var user = new AppUser
{
Email = email,
NormalizedEmail = normalized,
DisplayName = string.IsNullOrWhiteSpace(displayName) ? null : displayName.Trim(),
CreatedAtUtc = DateTime.UtcNow,
};
user.PasswordHash = passwordHasher.HashPassword(user, password);
await mongo.InsertAsync(user, ct);
await SignInUserAsync(user);
return (true, null);
}
public async Task<(bool Success, string? Error)> LoginAsync(
string email, string password, CancellationToken ct = default)
{
var normalized = email.ToUpperInvariant();
var user = await mongo.FindByNormalizedEmailAsync(normalized, ct);
if (user is null)
return (false, "Invalid email or password.");
var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
if (result == PasswordVerificationResult.Failed)
return (false, "Invalid email or password.");
await SignInUserAsync(user);
return (true, null);
}
public async Task SignOutAsync()
{
var ctx = httpContextAccessor.HttpContext
?? throw new InvalidOperationException("HttpContext is not available.");
await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
private async Task SignInUserAsync(AppUser user)
{
var ctx = httpContextAccessor.HttpContext
?? throw new InvalidOperationException("HttpContext is not available.");
List<Claim> claims =
[
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Email),
new Claim(ClaimTypes.Email, user.Email),
];
if (!string.IsNullOrEmpty(user.DisplayName))
claims.Add(new Claim("DisplayName", user.DisplayName));
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
}
}