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:
@@ -0,0 +1,114 @@
|
||||
using SurrealDb.Net;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Infrastructure.Persistence;
|
||||
|
||||
public sealed class ConsentService : IConsentService
|
||||
{
|
||||
private const string SelectConsentFields =
|
||||
"type::string(id) AS id, type::string(user) AS user, scope, granted_at, expires_at, revoked_at";
|
||||
|
||||
private readonly ISurrealDbClient _client;
|
||||
|
||||
public ConsentService(ISurrealDbClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<ConsentResponse> GrantAsync(
|
||||
string userId,
|
||||
GrantConsentRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (userTable, userIdPart) = SurrealRecordId.Split(userId);
|
||||
var consentId = Guid.NewGuid().ToString("N");
|
||||
var expiresClause = request.ExpiresAt.HasValue ? ", expires_at = $expiresAt" : string.Empty;
|
||||
|
||||
var parameters = new Dictionary<string, object?>
|
||||
{
|
||||
["consentId"] = consentId,
|
||||
["userTable"] = userTable,
|
||||
["userId"] = userIdPart,
|
||||
["scope"] = request.Scope,
|
||||
};
|
||||
|
||||
if (request.ExpiresAt.HasValue)
|
||||
{
|
||||
parameters["expiresAt"] = request.ExpiresAt;
|
||||
}
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
"CREATE type::record('consent', $consentId) SET " +
|
||||
"user = type::record($userTable, $userId), scope = $scope, " +
|
||||
"granted_at = time::now()" + expiresClause + "; " +
|
||||
$"SELECT {SelectConsentFields} FROM type::record('consent', $consentId) LIMIT 1;",
|
||||
parameters,
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<ConsentRow>>(1) ?? [];
|
||||
|
||||
return Map(rows[0]);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ConsentResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (userTable, userIdPart) = SurrealRecordId.Split(userId);
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
$"SELECT {SelectConsentFields} FROM consent " +
|
||||
"WHERE user = type::record($userTable, $userId) ORDER BY granted_at DESC;",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["userTable"] = userTable,
|
||||
["userId"] = userIdPart,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<ConsentRow>>(0) ?? [];
|
||||
|
||||
return rows.Select(Map).ToList();
|
||||
}
|
||||
|
||||
public async Task<bool> RevokeAsync(
|
||||
string consentId,
|
||||
string userId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (userTable, userIdPart) = SurrealRecordId.Split(userId);
|
||||
var (consentTable, consentIdPart) = SurrealRecordId.Split(consentId);
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
"UPDATE type::record($consentTable, $consentIdPart) SET revoked_at = time::now() " +
|
||||
"WHERE user = type::record($userTable, $userId) AND revoked_at = NONE; " +
|
||||
$"SELECT {SelectConsentFields} FROM type::record($consentTable, $consentIdPart) LIMIT 1;",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["consentTable"] = consentTable,
|
||||
["consentIdPart"] = consentIdPart,
|
||||
["userTable"] = userTable,
|
||||
["userId"] = userIdPart,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<ConsentRow>>(1) ?? [];
|
||||
|
||||
return rows.Count > 0 && rows[0].RevokedAt is not null;
|
||||
}
|
||||
|
||||
private static ConsentResponse Map(ConsentRow row) => new(
|
||||
row.Id,
|
||||
row.User,
|
||||
row.Scope,
|
||||
row.GrantedAt,
|
||||
row.ExpiresAt,
|
||||
row.RevokedAt);
|
||||
}
|
||||
Reference in New Issue
Block a user