diff options
Diffstat (limited to 'lib/VNLib.Plugins.Essentials.Users/src')
5 files changed, 913 insertions, 0 deletions
diff --git a/lib/VNLib.Plugins.Essentials.Users/src/Model/UserEntry.cs b/lib/VNLib.Plugins.Essentials.Users/src/Model/UserEntry.cs new file mode 100644 index 0000000..bc14076 --- /dev/null +++ b/lib/VNLib.Plugins.Essentials.Users/src/Model/UserEntry.cs @@ -0,0 +1,92 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Users +* File: UserEntry.cs +* +* UserEntry.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.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Plugins.Extensions.Data; +using VNLib.Plugins.Extensions.Data.Abstractions; + +namespace VNLib.Plugins.Essentials.Users.Model +{ + + /// <summary> + /// An efcore model of the lowest level of the user's entry + /// in the table + /// </summary> + [Index(nameof(UserId), IsUnique = true)] + public class UserEntry : DbModelBase, IUserEntity + { + /// <summary> + /// The Unique ID of the user + /// </summary> + [Key] + [MaxLength(64)] +#pragma warning disable CS8764 // Nullability of return type doesn't match overridden member (possibly because of nullability attributes). + public override string? Id { get; set; } +#pragma warning restore CS8764 // Nullability of return type doesn't match overridden member (possibly because of nullability attributes). + + /// <summary> + /// The secondary ID of the user, usually an EmailAddress + /// </summary> + [MaxLength(64)] + public string? UserId { get; set; } + + ///<inheritdoc/> + public override DateTime Created { get; set; } + + ///<inheritdoc/> + public override DateTime LastModified { get; set; } + + /// <summary> + /// The user's privilage flags + /// </summary> + public long PrivilegeLevel { get; set; } + + /// <summary> + /// The json-encoded raw user-data + /// </summary> + public byte[]? UserData { get; set; } + + /// <summary> + /// The optional unguarded password hash of the user entry + /// </summary> + [MaxLength(1000)] + public string? PassHash { get; set; } + + /// <summary> + /// A referrence to the <see cref="UserId"/> + /// parameter + /// </summary> + [NotMapped] + public string? EmailAddress + { + get => UserId; + set => UserId = value; + } + } +}
\ No newline at end of file diff --git a/lib/VNLib.Plugins.Essentials.Users/src/Model/UsersContext.cs b/lib/VNLib.Plugins.Essentials.Users/src/Model/UsersContext.cs new file mode 100644 index 0000000..09f992e --- /dev/null +++ b/lib/VNLib.Plugins.Essentials.Users/src/Model/UsersContext.cs @@ -0,0 +1,80 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Users +* File: UsersContext.cs +* +* UsersContext.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 Microsoft.EntityFrameworkCore; + +using VNLib.Plugins.Essentials.Accounts; +using VNLib.Plugins.Extensions.Data; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Loading.Sql; + +namespace VNLib.Plugins.Essentials.Users.Model +{ + /// <summary> + /// The Efcore transactional database context + /// </summary> + public class UsersContext : DBContextBase, IDbTableDefinition + { + /// <summary> + /// The Users table + /// </summary> + public DbSet<UserEntry> Users { get; set; } + +#nullable disable + + public UsersContext() + { } + + public UsersContext(DbContextOptions options):base(options) + { } + +#nullable enable + + + ///<inheritdoc/> + public void OnDatabaseCreating(IDbContextBuilder builder, object? userState) + { + PluginBase plugin = (userState as PluginBase)!; + + //Try to get the configuration for the users implementation + IConfigScope? userConfig = plugin.TryGetConfig("users"); + + //Maxium char size in most dbs + int userMaxLen = userConfig?.GetValueOrDefault<int>("max_data_size", 8000) ?? 8000; + + //Define user-table from the users dbset field + builder.DefineTable<UserEntry>(nameof(Users), table => + { + table.WithColumn(p => p.Id).AllowNull(false); + table.WithColumn(p => p.UserId).AllowNull(false); + table.WithColumn(p => p.LastModified); + table.WithColumn(p => p.Created); + table.WithColumn(p => p.PassHash); + table.WithColumn(p => p.PrivilegeLevel).AllowNull(false).WithDefault(AccountUtil.MINIMUM_LEVEL); + table.WithColumn(p => p.UserData).MaxLength(userMaxLen); + table.WithColumn(p => p.Version); + }); + } + } +}
\ No newline at end of file diff --git a/lib/VNLib.Plugins.Essentials.Users/src/UserData.cs b/lib/VNLib.Plugins.Essentials.Users/src/UserData.cs new file mode 100644 index 0000000..204a5c7 --- /dev/null +++ b/lib/VNLib.Plugins.Essentials.Users/src/UserData.cs @@ -0,0 +1,259 @@ +/* +* 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 +{ + + /// <summary> + /// Represents a user and its entry in the primary user table + /// </summary> + 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<string, string>? UserStorage { get; set; } + } + + private readonly Lazy<UserDataObj> Properties; + internal readonly UserEntry Entry; + + ///<inheritdoc/> + 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<UserDataObj>(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(); + } + + ///<inheritdoc/> + public string UserID => Entry.Id!; + + ///<inheritdoc/> + 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; + } + } + + ///<inheritdoc/> + public ulong Privileges + { + get => (ulong)Entry.PrivilegeLevel; + set + { + Check(); + //Set modified flag if changed + Modified |= (ulong)Entry.PrivilegeLevel != value; + Entry.PrivilegeLevel = unchecked((long)value); + } + } + + ///<inheritdoc/> + public DateTimeOffset Created => Entry.Created; + + ///<inheritdoc/> + public DateTimeOffset LastActive + { + get => DateTimeOffset.FromUnixTimeMilliseconds(Properties.Value.LastActive ?? 0); + set + { + long unixMs = value.ToUnixTimeMilliseconds(); + Modified |= Properties.Value.LastActive != unixMs; + Properties.Value.LastActive = unixMs; + } + } + + ///<inheritdoc/> + public UserStatus Status + { + get => (Properties.Value.Status ?? UserStatus.Unverified); + set + { + Modified |= Properties.Value.Status != value; + Properties.Value.Status = value; + } + } + + ///<inheritdoc/> + public bool LocalOnly + { + get => Properties.Value.LocalOnly ?? false; + set + { + Modified |= Properties.Value.LocalOnly != value; + Properties.Value.LocalOnly = value ? true : null; + } + } + + + /// <summary> + /// Users datastore of key-value string pairs + /// </summary> + /// <param name="key">Key for item in store</param> + /// <returns>The value string if found, string.Empty otherwise</returns> + 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 + + ///<inheritdoc/> + public T GetObject<T>(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<T>(prop, SR_OPTIONS) + : default; + } + + ///<inheritdoc/> + public void SetObject<T>(string key, T obj) + { + Check(); + + this[key] = obj == null ? null : JsonSerializer.Serialize(obj, SR_OPTIONS); + } + +#nullable enable + + ///<inheritdoc/> + public IEnumerator<KeyValuePair<string, string>> GetEnumerator() + { + Check(); + + return Properties.Value.UserStorage != null + ? Properties.Value.UserStorage.GetEnumerator() + : (IEnumerator<KeyValuePair<string, string>>)new Dictionary<string, string>.Enumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + ///<inheritdoc/> + public void Dispose() + { + if (!Disposed) + { + Properties.Value.UserStorage?.Clear(); + GC.SuppressFinalize(this); + Disposed = true; + } + } + + ///<inheritdoc/> + protected override object GetResource() + { + //Update user-data + Entry.UserData = JsonSerializer.SerializeToUtf8Bytes(Properties.Value, SR_OPTIONS); + return Entry; + } + } +}
\ No newline at end of file diff --git a/lib/VNLib.Plugins.Essentials.Users/src/UserManager.cs b/lib/VNLib.Plugins.Essentials.Users/src/UserManager.cs new file mode 100644 index 0000000..e8a0721 --- /dev/null +++ b/lib/VNLib.Plugins.Essentials.Users/src/UserManager.cs @@ -0,0 +1,417 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Users +* File: UserManagerExport.cs +* +* UserManagerExport.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.Data; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Hashing; +using VNLib.Utils; +using VNLib.Utils.Async; +using VNLib.Utils.Memory; +using VNLib.Plugins.Essentials.Accounts; +using VNLib.Plugins.Essentials.Users.Model; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Loading.Sql; +using VNLib.Plugins.Extensions.Loading.Users; + +namespace VNLib.Plugins.Essentials.Users +{ + + /// <summary> + /// Provides SQL database backed structured user accounts + /// </summary> + [ServiceExport] + [ConfigurationName("users", Required = false)] + public sealed class UserManager : IUserManager, IAsyncResourceStateHandler + { + + private readonly IAsyncLazy<DbContextOptions> _dbOptions; + private readonly IPasswordHashingProvider _passwords; + + public UserManager(PluginBase plugin) + { + //Get the connection factory + _dbOptions = plugin.GetContextOptionsAsync(); + + //Load password hashing provider + _passwords = plugin.GetOrCreateSingleton<ManagedPasswordHashing>(); + +#pragma warning disable CA5394 // Do not use insecure randomness + int randomDelay = Random.Shared.Next(1000, 4000); +#pragma warning restore CA5394 // Do not use insecure randomness + + //Create tables, but give plenty of delay on startup + _ = plugin.ObserveWork(() => CreateDatabaseTables(plugin), randomDelay); + } + + public UserManager(PluginBase plugin, IConfigScope config):this(plugin) + { } + + /* + * Create the databases! + */ + private static async Task CreateDatabaseTables(PluginBase plugin) + { + //Ensure the database is created + await plugin.EnsureDbCreatedAsync<UsersContext>(plugin); + } + + ///<inheritdoc/> + public IPasswordHashingProvider? GetHashProvider() => _passwords; + + ///<inheritdoc/> + public string ComputeSafeUserId(string input) + { + return ManagedHash.ComputeHash(input, HashAlg.SHA1, HashEncodingMode.Hexadecimal); + } + + private static string GetSafeRandomId() + { + return RandomHash.GetRandomHash(HashAlg.SHA1, 64, HashEncodingMode.Hexadecimal); + } + + ///<inheritdoc/> + public async Task<IUser> CreateUserAsync(IUserCreationRequest creation, string? userId, CancellationToken cancellation = default) + { + ArgumentNullException.ThrowIfNull(creation); + + //Set random user-id if not set + userId ??= GetSafeRandomId(); + ArgumentException.ThrowIfNullOrWhiteSpace(creation.Username, nameof(creation.Username)); + + PrivateString? hash = null; + + /* + * If a raw password is not required, it may be optionally left + * null for a random password to be generated. Otherwise the + * supplied password is hashed and stored. + */ + if(!creation.UseRawPassword) + { + hash = creation.Password == null ? + _passwords.GetRandomPassword() + : _passwords.Hash(creation.Password); + } + + try + { + //Init db + await using UsersContext db = new(_dbOptions.Value); + + //See if user exists by its id or by its email + bool exists = await (from s in db.Users + where s.Id == userId || s.UserId == creation.Username + select s) + .AnyAsync(cancellation); + if (exists) + { + //Rollback transaction + await db.SaveAndCloseAsync(false, cancellation); + + throw new UserExistsException("The user already exists"); + } + + DateTime now = DateTime.UtcNow; + + //Create user entry + UserEntry usr = new() + { + Id = userId, + UserId = creation.Username, + PrivilegeLevel = (long)creation.Privileges, + UserData = null, + Created = now, + LastModified = now, + + //Cast private string for storage + PassHash = (string?)(creation.UseRawPassword ? creation.Password : hash), + }; + + //Add to user table + db.Users.Add(usr); + + //Save changes + ERRNO count = await db.SaveAndCloseAsync(true, cancellation); + + //Remove ref to password hash + usr.PassHash = null; + + if (count) + { + return new UserData(this, usr) + { + Status = creation.InitialStatus + }; + } + + throw new UserCreationFailedException($"Failed to create the new user due to a database error. result: {count}"); + } + catch (UserExistsException) + { + throw; + } + catch (UserCreationFailedException) + { + throw; + } + catch (Exception ex) + { + throw new UserCreationFailedException("Failed to create the user account", ex); + } + finally + { + hash?.Erase(); + } + } + + ///<inheritdoc/> + public async Task<ERRNO> ValidatePasswordAsync(IUser user, PrivateString password, PassValidateFlags flags, CancellationToken cancellation = default) + { + ArgumentNullException.ThrowIfNull(user); + ArgumentNullException.ThrowIfNull(password); + + //Try to get the user's password or hash + using PrivateString? passHash = await RecoverPasswordAsync(user, cancellation); + + if(passHash is null) + { + return UserPassValResult.Null; + } + + //See if hashing is bypassed + if ((flags & PassValidateFlags.BypassHashing) > 0) + { + //Compare raw passwords + return password.Equals(passHash) + ? UserPassValResult.Success + : UserPassValResult.Failed; + } + else + { + //Verify password hashes (usually defauly) + return _passwords.Verify(passHash, password) + ? UserPassValResult.Success + : UserPassValResult.Failed; + } + } + + ///<inheritdoc/> + public async Task<PrivateString?> RecoverPasswordAsync(IUser user, CancellationToken cancellation = default) + { + ArgumentNullException.ThrowIfNull(user); + + await using UsersContext db = new(_dbOptions.Value); + + //Get a user entry that only contains the password hash and user-id + UserEntry? usr = await (from s in db.Users + where s.Id == user.UserID + select new UserEntry + { + Id = s.Id, + PassHash = s.PassHash, + Version = s.Version + }) + .SingleOrDefaultAsync(cancellation); + + //Close transactions and return + await db.SaveAndCloseAsync(true, cancellation); + + //Convert to private string + return PrivateString.ToPrivateString(usr?.PassHash, true); + } + + ///<inheritdoc/> + public async Task<ERRNO> UpdatePasswordAsync(IUser user, PrivateString newPass, CancellationToken cancellation = default) + { + ArgumentNullException.ThrowIfNull(newPass); + ArgumentException.ThrowIfNullOrEmpty((string?)newPass); + + //Get the entry back from the user data object + UserEntry entry = user is UserData ue ? ue.Entry : throw new ArgumentException("User must be a UserData object", nameof(user)); + + await using UsersContext db = new(_dbOptions.Value); + + //Track the entry again + db.Users.Attach(entry); + + //Compute the new password hash + using PrivateString passwordHash = _passwords.Hash(newPass); + + //Update password (must cast) + entry.PassHash = (string?)passwordHash; + + //Update modified time + entry.LastModified = DateTime.UtcNow; + + //Save changes async + int count = await db.SaveAndCloseAsync(true, cancellation); + + //Clear the new password hash + entry.PassHash = null; + + return count; + } + + ///<inheritdoc/> + public async Task<long> GetUserCountAsync(CancellationToken cancellation = default) + { + await using UsersContext db = new(_dbOptions.Value); + + long count = await db.Users.LongCountAsync(cancellation); + + await db.SaveAndCloseAsync(true, cancellation); + + return count; + } + + [Obsolete("Removed in favor of GetUserFromUsernameAsync, transition away from email address")] + public Task<IUser?> GetUserFromEmailAsync(string emailAddress, CancellationToken cancellation = default) + => GetUserFromUsernameAsync(emailAddress, cancellation); + + ///<inheritdoc/> + public async Task<IUser?> GetUserFromUsernameAsync(string username, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(username); + + await using UsersContext db = new(_dbOptions.Value); + + //Get user without password + UserEntry? usr = await (from s in db.Users + where s.UserId == username + select new UserEntry + { + Id = s.Id, + Created = s.Created, + UserId = s.UserId, + LastModified = s.LastModified, + PassHash = null, + PrivilegeLevel = s.PrivilegeLevel, + UserData = s.UserData, + Version = s.Version + }) + .SingleOrDefaultAsync(cancellationToken); + + //Close transactions and return + await db.SaveAndCloseAsync(true, cancellationToken); + + return usr == null ? null : new UserData(this, usr); + } + + ///<inheritdoc/> + public async Task<IUser?> GetUserFromIDAsync(string userId, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(userId); + + await using UsersContext db = new(_dbOptions.Value); + + //Get user without a password + UserEntry? usr = await (from s in db.Users + where s.Id == userId + select new UserEntry + { + Id = s.Id, + Created = s.Created, + UserId = s.UserId, + LastModified = s.LastModified, + PassHash = null, + PrivilegeLevel = s.PrivilegeLevel, + UserData = s.UserData, + Version = s.Version + }) + .SingleOrDefaultAsync(cancellationToken); + + + //Close transactions and return + await db.SaveAndCloseAsync(true, cancellationToken); + + return usr == null + ? null + : new UserData(this, usr); + } + + ///<inheritdoc/> + async Task IAsyncResourceStateHandler.UpdateAsync(AsyncUpdatableResource resource, object state, CancellationToken cancellation) + { + //recover user-data object + UserEntry entry = (state as UserEntry)!; + ERRNO result; + try + { + await using UsersContext db = new(_dbOptions.Value); + + //Track the entry again + db.Users.Attach(entry); + + //Set all mutable entry modified flags + db.Entry(entry).Property(x => x.UserData).IsModified = true; + db.Entry(entry).Property(x => x.PrivilegeLevel).IsModified = true; + + //Update modified time + entry.LastModified = DateTime.UtcNow; + + + result = await db.SaveAndCloseAsync(true, cancellation); + } + catch (Exception ex) + { + throw new UserUpdateException("", ex); + } + if (!result) + { + throw new UserUpdateException("The update operation failed because the transaction returned 0 updated records", null); + } + } + + ///<inheritdoc/> + async Task IAsyncResourceStateHandler.DeleteAsync(AsyncUpdatableResource resource, CancellationToken cancellation) + { + //recover user-data object + UserData user = (resource as UserData)!; + ERRNO result; + try + { + await using UsersContext db = new(_dbOptions.Value); + + //Delete the user from the database + db.Users.Remove(user.Entry); + + result = await db.SaveAndCloseAsync(true, cancellation); + } + catch (Exception ex) + { + throw new UserDeleteException("Failed to delete the user entry from the database", ex); + } + if (!result) + { + throw new UserDeleteException("Failed to delete the user account because of a database failure, the user may already be deleted", null); + } + } + + } +} diff --git a/lib/VNLib.Plugins.Essentials.Users/src/VNLib.Plugins.Essentials.Users.csproj b/lib/VNLib.Plugins.Essentials.Users/src/VNLib.Plugins.Essentials.Users.csproj new file mode 100644 index 0000000..eed0bba --- /dev/null +++ b/lib/VNLib.Plugins.Essentials.Users/src/VNLib.Plugins.Essentials.Users.csproj @@ -0,0 +1,65 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <Nullable>enable</Nullable> + <AssemblyName>VNLib.Plugins.Essentials.Users</AssemblyName> + <RootNamespace>VNLib.Plugins.Essentials.Users</RootNamespace> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <!--Enable dynamic loading--> + <EnableDynamicLoading>true</EnableDynamicLoading> + </PropertyGroup> + + <PropertyGroup> + <AnalysisLevel Condition="'$(BuildingInsideVisualStudio)' == true">latest-all</AnalysisLevel> + </PropertyGroup> + + <PropertyGroup> + <PackageId>VNLib.Plugins.Essentials.Users</PackageId> + <Authors>Vaughn Nugent</Authors> + <Company>Vaughn Nugent</Company> + <Product>VNLib.Plugins.Essentials.Users</Product> + <Copyright>Copyright © 2024 Vaughn Nugent</Copyright> + <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials</PackageProjectUrl> + <RepositoryUrl>https://github.com/VnUgE/Plugins.Essentials/tree/master/lib/VNLib.Plugins.Essentials.Users</RepositoryUrl> + <Description>Provides a runtime loadable IUserManager and IUser implementation, backed by an SQL database. This project may be stored in your plugin's Assets directory to be loaded at runtime by the Extensions.Loading.UserManger singleton.</Description> + <PackageReadmeFile>README.md</PackageReadmeFile> + <PackageLicenseFile>LICENSE</PackageLicenseFile> + <RequireLicenseAcceptance>True</RequireLicenseAcceptance> + </PropertyGroup> + + <ItemGroup> + <None Include="..\..\..\LICENSE"> + <Pack>True</Pack> + <PackagePath>\</PackagePath> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Include="..\README.md"> + <Pack>True</Pack> + <PackagePath>\</PackagePath> + </None> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\..\core\lib\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" /> + <ProjectReference Include="..\..\..\..\VNLib.Plugins.Extensions\lib\VNLib.Plugins.Extensions.Data\src\VNLib.Plugins.Extensions.Data.csproj" /> + <ProjectReference Include="..\..\..\..\VNLib.Plugins.Extensions\lib\VNLib.Plugins.Extensions.Loading.Sql\src\VNLib.Plugins.Extensions.Loading.Sql.csproj" /> + <ProjectReference Include="..\..\..\..\VNLib.Plugins.Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" /> + </ItemGroup> + + <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent"> + <Exec Command="start xcopy "$(TargetDir)" "$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> + </Target> + +</Project> |