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;
|
||||
using TarninaHub.Application.Models;
|
||||
|
||||
namespace TarninaHub.Infrastructure.Persistence;
|
||||
|
||||
public sealed class UserService : IUserService
|
||||
{
|
||||
private const string SelectUserFields =
|
||||
"type::string(id) AS id, phone, email, role, type::string(organization) AS organization";
|
||||
|
||||
private readonly ISurrealDbClient _client;
|
||||
|
||||
public UserService(ISurrealDbClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<UserAccount?> FindByPhoneAsync(string phone, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _client.RawQuery(
|
||||
$"SELECT {SelectUserFields} FROM user WHERE phone = $phone LIMIT 1;",
|
||||
new Dictionary<string, object?> { ["phone"] = phone },
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<UserRow>>(0) ?? [];
|
||||
|
||||
return rows.Count > 0 ? Map(rows[0]) : null;
|
||||
}
|
||||
|
||||
public async Task<UserAccount?> GetByIdAsync(string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var (table, id) = SurrealRecordId.Split(userId);
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
$"SELECT {SelectUserFields} FROM type::record($table, $id) LIMIT 1;",
|
||||
new Dictionary<string, object?> { ["table"] = table, ["id"] = id },
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<UserRow>>(0) ?? [];
|
||||
|
||||
return rows.Count > 0 ? Map(rows[0]) : null;
|
||||
}
|
||||
|
||||
public async Task<UserAccount?> CreateAsync(
|
||||
RegisterRequest request,
|
||||
string role,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var emailClause = string.IsNullOrWhiteSpace(request.Email) ? string.Empty : ", email = $email";
|
||||
|
||||
var parameters = new Dictionary<string, object?>
|
||||
{
|
||||
["phone"] = request.Phone,
|
||||
["password"] = request.Password,
|
||||
["role"] = role,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Email))
|
||||
{
|
||||
parameters["email"] = request.Email;
|
||||
}
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
"CREATE user SET phone = $phone" + emailClause + ", " +
|
||||
"password = crypto::argon2::generate($password), role = $role; " +
|
||||
$"SELECT {SelectUserFields} FROM user WHERE phone = $phone LIMIT 1;",
|
||||
parameters,
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<UserRow>>(1) ?? [];
|
||||
|
||||
return rows.Count > 0 ? Map(rows[0]) : null;
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyCredentialsAsync(
|
||||
string phone,
|
||||
string password,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _client.RawQuery(
|
||||
"SELECT crypto::argon2::compare(password, $password) AS password_ok " +
|
||||
"FROM user WHERE phone = $phone LIMIT 1;",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["phone"] = phone,
|
||||
["password"] = password,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<PasswordCheckRow>>(0) ?? [];
|
||||
|
||||
return rows.Count > 0 && rows[0].PasswordOk;
|
||||
}
|
||||
|
||||
private static UserAccount Map(UserRow row) => new(
|
||||
row.Id,
|
||||
row.Phone,
|
||||
string.IsNullOrWhiteSpace(row.Email) ? null : row.Email,
|
||||
row.Role,
|
||||
NormalizeRecordId(row.Organization));
|
||||
|
||||
private static string? NormalizeRecordId(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) || value is "NONE" or "NULL" ? null : value;
|
||||
}
|
||||
Reference in New Issue
Block a user