/* * Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Users * File: UserData.cs * * UserData.cs is part of VNLib.Plugins.Essentials.Users which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Essentials.Users is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 2 of the License, * or (at your option) any later version. * * VNLib.Plugins.Essentials.Users is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with VNLib.Plugins.Essentials.Users. If not, see http://www.gnu.org/licenses/. */ using System; using System.Text.Json; using System.Collections; using System.Collections.Generic; using System.Text.Json.Serialization; using VNLib.Utils.Async; using VNLib.Plugins.Essentials.Users.Model; using static VNLib.Plugins.Essentials.Statics; namespace VNLib.Plugins.Essentials.Users { /// /// Represents a user and its entry in the primary user table /// public sealed class UserData : AsyncUpdatableResource, IUser { private sealed class UserDataObj { [JsonPropertyName("la")] public long? LastActive { get; set; } [JsonPropertyName("st")] public UserStatus? Status { get; set; } [JsonPropertyName("lo")] public bool? LocalOnly { get; set; } [JsonPropertyName("us")] public Dictionary? UserStorage { get; set; } } private readonly Lazy Properties; internal readonly UserEntry Entry; /// protected override IAsyncResourceStateHandler AsyncHandler { get; } private bool Disposed; internal UserData(IAsyncResourceStateHandler handler, UserEntry entry) { //Init the callbacks in async mode Entry = entry; AsyncHandler = handler; //Undef the password hash in the entry entry.PassHash = null; //Lazy properties Properties = new(LoadData, false); } private UserDataObj LoadData() { UserDataObj? props = null; try { //Recover properties from stream props = JsonSerializer.Deserialize(Entry.UserData, SR_OPTIONS) ?? new UserDataObj(); } //Catch json exception for invalid data, propagate other exceptions catch (JsonException) { //If an exception was thrown reading back the data object, set modified flag to overwrite on release Modified = true; } //If props is null (or an exception is thrown, return props ?? new(); } /// public string UserID => Entry.Id!; /// public string EmailAddress { get => Entry.EmailAddress!; set { Check(); ArgumentException.ThrowIfNullOrEmpty(value, nameof(EmailAddress)); //Set modified flag if changed Modified |= Entry.EmailAddress!.Equals(value, StringComparison.OrdinalIgnoreCase); Entry.EmailAddress = value; } } /// public ulong Privileges { get => (ulong)Entry.PrivilegeLevel; set { Check(); //Set modified flag if changed Modified |= (ulong)Entry.PrivilegeLevel != value; Entry.PrivilegeLevel = unchecked((long)value); } } /// public DateTimeOffset Created => Entry.Created; /// public DateTimeOffset LastActive { get => DateTimeOffset.FromUnixTimeMilliseconds(Properties.Value.LastActive ?? 0); set { long unixMs = value.ToUnixTimeMilliseconds(); Modified |= Properties.Value.LastActive != unixMs; Properties.Value.LastActive = unixMs; } } /// public UserStatus Status { get => (Properties.Value.Status ?? UserStatus.Unverified); set { Modified |= Properties.Value.Status != value; Properties.Value.Status = value; } } /// public bool LocalOnly { get => Properties.Value.LocalOnly ?? false; set { Modified |= Properties.Value.LocalOnly != value; Properties.Value.LocalOnly = value ? true : null; } } /// /// Users datastore of key-value string pairs /// /// Key for item in store /// The value string if found, string.Empty otherwise public string this[string key] { get { Check(); string? val = null; Properties.Value.UserStorage?.TryGetValue(key, out val); return val ?? ""; } set { Check(); //If the value is null, see if the the properties are null if (string.IsNullOrWhiteSpace(value)) { //If properties are null exit if (Properties.Value.UserStorage != null) { //If the value is null and properies exist, remove the entry Properties.Value.UserStorage.Remove(key); Modified = true; } } else { Properties.Value.UserStorage ??= new(); //Set the value Properties.Value.UserStorage[key] = value; //Set modified flag Modified = true; } } } #nullable disable /// public T GetObject(string key) { Check(); //If user storage has been definied, then try to get the value return Properties.Value.UserStorage?.TryGetValue(key, out string prop) == true ? JsonSerializer.Deserialize(prop, SR_OPTIONS) : default; } /// public void SetObject(string key, T obj) { Check(); this[key] = obj == null ? null : JsonSerializer.Serialize(obj, SR_OPTIONS); } #nullable enable /// public IEnumerator> GetEnumerator() { Check(); return Properties.Value.UserStorage != null ? Properties.Value.UserStorage.GetEnumerator() : (IEnumerator>)new Dictionary.Enumerator(); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// public void Dispose() { if (!Disposed) { Properties.Value.UserStorage?.Clear(); GC.SuppressFinalize(this); Disposed = true; } } /// protected override object GetResource() { //Update user-data Entry.UserData = JsonSerializer.SerializeToUtf8Bytes(Properties.Value, SR_OPTIONS); return Entry; } } }