using System.ComponentModel.DataAnnotations.Schema; using System.Security.Cryptography; using System.Text; using Cysharp.Text; using OliverBooth.Common.Data.Blog; using OtpNet; namespace OliverBooth.Common.Data.Web.Users; /// /// Represents a user. /// internal sealed class User : IUser, IAuthor { /// [NotMapped] public Uri AvatarUrl => GetAvatarUrl(); /// public string EmailAddress { get; set; } = string.Empty; /// public string DisplayName { get; set; } = string.Empty; /// public Guid Id { get; private set; } = Guid.NewGuid(); /// public IReadOnlyList Permissions { get; private set; } = ArraySegment.Empty; /// public DateTimeOffset Registered { get; private set; } = DateTimeOffset.UtcNow; /// public string? Totp { get; private set; } /// /// Gets or sets the password hash. /// /// The password hash. internal string Password { get; set; } = string.Empty; /// /// Gets or sets the salt used to hash the password. /// /// The salt used to hash the password. internal string Salt { get; set; } = string.Empty; /// public Uri GetAvatarUrl(int size = 28) { if (string.IsNullOrWhiteSpace(EmailAddress)) { return new Uri($"https://www.gravatar.com/avatar/0?size={size}"); } ReadOnlySpan span = EmailAddress.AsSpan(); int byteCount = Encoding.UTF8.GetByteCount(span); Span bytes = stackalloc byte[byteCount]; Encoding.UTF8.GetBytes(span, bytes); Span hash = stackalloc byte[16]; MD5.TryHashData(bytes, hash, out _); using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder(); Span hex = stackalloc char[2]; for (var index = 0; index < hash.Length; index++) { if (hash[index].TryFormat(hex, out _, "x2")) { builder.Append(hex); } else { builder.Append("00"); } } return new Uri($"https://www.gravatar.com/avatar/{builder}?size={size}"); } /// public bool HasPermission(Permission permission) { return HasPermission(permission.Name); } /// public bool HasPermission(string permission) { return (Permissions.Any(p => p.IsAllowed && p.Name == permission) || Permissions.Any(p => p is { IsAllowed: true, Name: "*" })) && !Permissions.Any(p => !p.IsAllowed && p.Name == permission); } /// public bool TestCredentials(string password) { return false; } /// public bool TestTotp(string value) { byte[]? key = Base32Encoding.ToBytes(Totp); var totp = new Totp(key); return totp.VerifyTotp(value, out _, VerificationWindow.RfcSpecifiedNetworkDelay); } }