feat(backend): add JWT authentication, RBAC and consent/audit services

Add signup/signin with backend-issued JWT (Argon2 passwords in SurrealDB), role-based authorization policies, per-user data access levels, consent register and read audit log. Protect user endpoints and record sensitive reads.
This commit is contained in:
Rafał Tyński
2026-10-04 02:11:56 +02:00
parent 24c9e582c9
commit 482a4b41f0
26 changed files with 896 additions and 0 deletions
@@ -0,0 +1,11 @@
namespace TarninaHub.Application.Abstractions;
public interface IAuditLogService
{
Task RecordAsync(
string? actorId,
string action,
string target,
IReadOnlyDictionary<string, object?> context,
CancellationToken cancellationToken);
}
@@ -0,0 +1,20 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IConsentService
{
Task<ConsentResponse> GrantAsync(
string userId,
GrantConsentRequest request,
CancellationToken cancellationToken);
Task<IReadOnlyList<ConsentResponse>> ListForUserAsync(
string userId,
CancellationToken cancellationToken);
Task<bool> RevokeAsync(
string consentId,
string userId,
CancellationToken cancellationToken);
}
@@ -0,0 +1,10 @@
using TarninaHub.Application.Models;
namespace TarninaHub.Application.Abstractions;
public interface ITokenService
{
TokenResult CreateToken(UserAccount user);
}
public sealed record TokenResult(string AccessToken, DateTimeOffset ExpiresAt);
@@ -0,0 +1,15 @@
using TarninaHub.Application.Dtos;
using TarninaHub.Application.Models;
namespace TarninaHub.Application.Abstractions;
public interface IUserService
{
Task<UserAccount?> FindByPhoneAsync(string phone, CancellationToken cancellationToken);
Task<UserAccount?> GetByIdAsync(string userId, CancellationToken cancellationToken);
Task<UserAccount?> CreateAsync(RegisterRequest request, string role, CancellationToken cancellationToken);
Task<bool> VerifyCredentialsAsync(string phone, string password, CancellationToken cancellationToken);
}
+18
View File
@@ -0,0 +1,18 @@
namespace TarninaHub.Application.Dtos;
public sealed record RegisterRequest(string Phone, string? Email, string Password);
public sealed record LoginRequest(string Phone, string Password);
public sealed record UserResponse(
string Id,
string Phone,
string? Email,
string Role,
string? OrganizationId);
public sealed record TokenResponse(
string AccessToken,
string TokenType,
DateTimeOffset ExpiresAt,
UserResponse User);
+11
View File
@@ -0,0 +1,11 @@
namespace TarninaHub.Application.Dtos;
public sealed record GrantConsentRequest(string Scope, DateTimeOffset? ExpiresAt);
public sealed record ConsentResponse(
string Id,
string UserId,
string Scope,
DateTimeOffset GrantedAt,
DateTimeOffset? ExpiresAt,
DateTimeOffset? RevokedAt);
@@ -0,0 +1,14 @@
using TarninaHub.Application.Dtos;
using TarninaHub.Application.Models;
namespace TarninaHub.Application.Mappings;
public static class UserMappings
{
public static UserResponse ToResponse(this UserAccount user) => new(
user.Id,
user.Phone,
user.Email,
user.Role,
user.OrganizationId);
}
@@ -0,0 +1,8 @@
namespace TarninaHub.Application.Models;
public sealed record UserAccount(
string Id,
string Phone,
string? Email,
string Role,
string? OrganizationId);
@@ -0,0 +1,12 @@
using System.Security.Claims;
namespace TarninaHub.Application.Security;
public static class ClaimsPrincipalExtensions
{
public static string? GetUserId(this ClaimsPrincipal principal) =>
principal.FindFirst("sub")?.Value;
public static string? GetRole(this ClaimsPrincipal principal) =>
principal.FindFirst("role")?.Value;
}
@@ -0,0 +1,47 @@
namespace TarninaHub.Application.Security;
public static class RoleNames
{
public const string User = "user";
public const string Jst = "jst";
public const string Operator = "operator";
public const string Manager = "manager";
public const string Admin = "admin";
public static readonly IReadOnlyList<string> All =
[User, Jst, Operator, Manager, Admin];
public static bool IsValid(string role) => All.Contains(role);
}
public static class Policies
{
/// <summary>operator, manager, admin — ROPS/expert staff.</summary>
public const string Staff = "Staff";
/// <summary>operator, admin.</summary>
public const string OperatorOrAdmin = "OperatorOrAdmin";
/// <summary>manager, operator, admin.</summary>
public const string ManagerOrAbove = "ManagerOrAbove";
/// <summary>admin only.</summary>
public const string AdminOnly = "AdminOnly";
/// <summary>jst and above (jst, operator, manager, admin).</summary>
public const string JstOrAbove = "JstOrAbove";
}
public static class ConsentScopes
{
/// <summary>Access granted by the data subject's consent.</summary>
public const string ByConsent = "by-consent";
/// <summary>Time-limited access (uses consent.expires_at).</summary>
public const string Temporary = "temporary";
/// <summary>Fully anonymized data (no personal identifiers).</summary>
public const string Anonymized = "anonymized";
public static readonly IReadOnlyList<string> All = [ByConsent, Temporary, Anonymized];
}