DbContext, Dal, entities, etc..

This commit is contained in:
jozsef.b@aycode.com 2023-12-10 14:13:19 +01:00
parent 905347552a
commit f8ca47f3aa
31 changed files with 1216 additions and 60 deletions

View File

@ -6,4 +6,12 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AyCode.Utils\AyCode.Utils.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Enums\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,104 @@
namespace AyCode.Core.Consts
{
public static class AcConst
{
public const int ImageUploadLimit = 4096 * 1024;
public static int ConsentCount = 1;
public static readonly Guid NullMessageInstanceId;
public const int MinFullUserNameLength = 6;
public const int MaxFullUserNameLength = 250;
public const int MinUserNameLength = 3;
public const int MaxUserNameLength = 255;
public const int MinPlayerNameLength = MinUserNameLength;
public const int MaxPlayerNameLength = MaxUserNameLength;
public const int MinPasswordLength = 6;
public const int MaxPasswordLength = 32;
public const int MinUserTokenLength = 8;
public const int MaxUserTokenLength = 12;
public const int MinDomainLength = 6;
public const int MaxDomainLength = 100;
public const int MinDomainExtensionLength = 2;
public const int MaxDomainExtensionLength = 15;
public const int MaxTcpBodyLengthDefault = 65535;
public const string NullMessageString = "_";
public const string EmptyMessageString = "#";
public const long NullMessageDateTime = -1;
public static readonly string AnataBrandFolder;
public static readonly string AnataRootCacheFolder;
public static readonly string AnataUserMediaCacheFolder;
public static readonly string AnataAssetBundleCacheFolder;
public static readonly Guid RegionDefaultTemplateId;
public static readonly Guid InitScene;
public static readonly Guid LobbyRoom;
/// <summary>
/// MainScene, Lobby
/// </summary>
public static readonly Guid AnataCity;
public static readonly Guid PlayerGarage;
public static readonly Guid PlayerWardrobe;
public static readonly Guid LoadingId;
public static readonly Guid GlobalChat;
/// <summary>
/// RegionAdmin
/// </summary>
public static readonly Guid RegionAdminUserId;
public static readonly Guid RegionAdminPlayerId;
public static readonly Guid GamePlayer;
public static readonly Guid AdultPlayer;
public static readonly Guid BusinessPlayer;
public static readonly Guid AnataWorldDomainId;
public static readonly Guid AnataWorldBrandId;
public static string[] AvailableDomainSuffixes = new string[] { "3d", "anata", "app", "art", "club", "game", "blog", "shop", "biz", "chat", "conf", "city", "com", "net", "cool", "dance", "date", "fun", "design", "digital", "estate", "metaverse", "mv", "events", "fans", "fashion", "makeup", "fin", "fm", "sport", "gallery", "io", "info", "job", "mev", "land", "world", "life", "live", "lol", "love", "market", "media", "museum", "news", "ngo", "ninja", "kiwi", "one", "org", "party", "pink", "press", "slide", "property", "pub", "race", "sale", "school", "science", "social", "study", "style", "support", "tattoo", "team", "tech", "theatre", "town", "trade", "travel", "tv", "university", "education", "video", "vip", "vision", "wiki", "work", "xxx", "yeti", "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as", "at", "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cw", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "eu", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sk", "sl", "sm", "sn", "so", "sr", "ss", "st", "su", "sv", "sx", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "ye", "yt", "za", "zm", "zw" };
static AcConst()
{
//var anataFolder = AcDomain.IsProductVersion ? "Anata" : "AnataDev";
//AnataRootCacheFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AyCode", anataFolder);
//AnataUserMediaCacheFolder = Path.Combine(AnataRootCacheFolder, "MediaContents");
//AnataAssetBundleCacheFolder = Path.Combine(AnataRootCacheFolder, "AssetBundles");
//AnataBrandFolder = Path.Combine(AnataRootCacheFolder, "Brands");
//NullMessageInstanceId = Guid.Empty;
//RegionDefaultTemplateId = Guid.Parse(AntDomain.IsProductVersion ? "6201b570-09f2-4cce-93b8-0e92d37b9b6b" : "ac087b5a-1dff-48c0-ab7b-c3a5defa0d9e");
//InitScene = Guid.Parse("2db0be7b-5393-4b0f-a3f6-868155a6e62f");
//LobbyRoom = Guid.Parse("73a7dbc6-c85f-40f7-88d5-d7baecdbb961");
//AnataCity = Guid.Parse("dec9a00c-3fad-438d-aaa4-2109a7d5451f");
//LoadingId = Guid.Parse("3d7b7c8c-3eed-4625-a727-0caf665d4208");
//PlayerGarage = Guid.Parse("6ec1f5d7-57e9-440f-a486-20be5782f86a");
//PlayerWardrobe = Guid.Parse("69acf032-dc75-4fae-badb-1fe8473f664f");
//GlobalChat = Guid.Parse("ab5efc63-c654-41cd-b0a6-fa6aecc02d58");
//RegionAdminUserId = Guid.Parse("51B53FCC-CE85-473D-8E00-F42A7F0DBE1D");
//RegionAdminPlayerId = Guid.Parse("4ADF8015-C127-4E98-A022-B7407DBD3A38");
//GamePlayer = Guid.Parse("E023500F-D024-4D9F-A8F9-ECDF54382961");
//AdultPlayer = Guid.Parse("6A5E3C40-2022-4DFF-A362-396B73F15A69");
//BusinessPlayer = Guid.Parse("F73BFDFC-3A92-4A14-B09C-F5ED22BF4027");
//AnataWorldDomainId = Guid.Parse("8EFA53B3-7114-41E2-98F1-D690ECF509D2");
//AnataWorldBrandId = Guid.Parse("99265775-4B19-4975-B338-EB0F2F890806");
}
}
}

View File

@ -0,0 +1,22 @@
//using Anata.Logger;
namespace AyCode.Core.Consts
{
//TODO: Adatbázisból kéne a környezeti beállításokat - J.
public static class AcEnv
{
/// <summary>
/// Environment.NewLine
/// </summary>
// ReSharper disable once InconsistentNaming
public static string NL => Environment.NewLine;
//public static AppModeType AppModeServer = AppModeType.Trace;
////Elküldeni a kliens-nek, így szerver oldalról tudjuk szabályozni...
//public static AppModeType AppModeClient = AppModeType.Trace;
//public static LogLevel LogLevel = LogLevel.Detail;
public static ulong MaxLogItemsPerSession = 250;
}
}

View File

@ -0,0 +1,57 @@
namespace AyCode.Core.Consts
{
public enum AcErrorCode : byte
{
Unset = 255,
//[DisplayName("Unknown error")]
UnknownError = 0,
InvalidMessage = 1,
SessionOpenFailed = 2,
ServerSideSessionOpenFailed = 3,
SessionNotFound = 4,
ServerSideSessionNotFound = 5,
DatabaseError = 6,
UserNameInUse = 7,
EmailInUse = 8,
UserAlreadyLoggedIn = 9,
WrongLoginData = 10,
UserWasntLoggedIn = 11,
NotAllowedMethod = 12,
EmailSendError = 13,
RoomFullOrHasStarted = 14,
UserIsLockOut = 15,
ClientVersionNotValid = 16,
PermissionTagDenied = 17,
RequestedGetItemNotfound = 18,
RequestedPostError = 19,
RequestedEditError = 20,
RequestedDeleteError = 21,
NotAllowedUserId = 22,
NotAllowedPlayerId = 23,
IdIsNullOrEmpty = 24,
IdNotFound = 25,
EntityIsNull = 26,
UserNameIsTooShort = 27,
PasswordIsTooShort = 28,
EmailFormatIsNotValid = 29,
UserTokenIsNotValid = 30,
ConsentNotAccepted = 31,
UserNameNotFound = 32,
PlayerNotFoundForUser = 33,
MarketItemExistsWithSameName = 34,
PlayerNameInUse = 35,
PlayerNameIsTooShort = 36,
ConfirmationPasswordNotValid = 37,
RegionPlayerMaxLimit = 38,
OwnerIsNotSpawnedInRegion = 39,
RegionDataNotFound = 40,
RegionNotFound = 41,
BrandIdNotFound = 42,
DomainIdNotFound = 43,
DisableAddUser = 44,
}
}

View File

@ -0,0 +1,73 @@
using AyCode.Utils.Extensions;
using System.Text.RegularExpressions;
namespace AyCode.Core.Consts
{
public static class AcValidate
{
public static bool IsValidUserNameAndPasswordFormat(string username, string password, out AcErrorCode acErrorCode)
{
return IsValidUserNameFormat(username, out acErrorCode) && IsValidPasswordFormat(password, out acErrorCode);
}
public static bool IsValidUserNameFormat(string userName, out AcErrorCode acErrorCode)
{
acErrorCode = AcErrorCode.Unset;
var isValid = !userName.IsNullOrWhiteSpace() && userName.Length is >= AcConst.MinUserNameLength and <= AcConst.MaxUserNameLength && userName.Trim() == userName;
if (!isValid) acErrorCode = AcErrorCode.UserNameIsTooShort;
return isValid;
}
public static bool IsValidPlayerNameFormat(string playerName, out AcErrorCode acErrorCode)
{
acErrorCode = AcErrorCode.Unset;
var isValid = !playerName.IsNullOrWhiteSpace() && playerName.Length is >= AcConst.MinPlayerNameLength and <= AcConst.MaxPlayerNameLength && playerName.Trim() == playerName;
if (!isValid) acErrorCode = AcErrorCode.PlayerNameIsTooShort;
return isValid;
}
public static bool IsValidPasswordFormat(string password, out AcErrorCode acErrorCode)
{
acErrorCode = AcErrorCode.Unset;
var isValid = !password.IsNullOrWhiteSpace() && password.Length is >= AcConst.MinPasswordLength and <= AcConst.MaxPasswordLength;
if (!isValid) acErrorCode = AcErrorCode.PasswordIsTooShort;
return isValid;
}
public static bool IsValidEmailFormat(string email, out AcErrorCode acErrorCode)
{
acErrorCode = AcErrorCode.Unset;
//a@a.a -> length == 5
var isValid = !email.IsNullOrWhiteSpace() && email.Length >= 5 && email.Contains('@') && email.Contains('.') && !email.Contains(' ');
if (!isValid) acErrorCode = AcErrorCode.EmailFormatIsNotValid;
return isValid;
}
public static bool IsValidUserTokenFormat(string verificationToken, out AcErrorCode acErrorCode)
{
acErrorCode = AcErrorCode.Unset;
var isValid = !verificationToken.IsNullOrWhiteSpace() && verificationToken.Length is >= AcConst.MinUserTokenLength and <= AcConst.MaxUserTokenLength;
if (!isValid) acErrorCode = AcErrorCode.UserTokenIsNotValid;
return isValid;
}
public static bool IsValidDomain(string domainName)
{
var suffixes = string.Join('|', AcConst.AvailableDomainSuffixes);
var regexp = new Regex("^[a-z0-9]{3,90}\\.(" + suffixes + ")$"); // a pattern-t vegyük ki konstansba a dll-re - B.
var match = regexp.IsMatch(domainName);
return match;
}
}
}

View File

@ -0,0 +1,70 @@
namespace AyCode.Core.Helpers
{
public static class TaskHelper
{
public static bool WaitTo(Func<bool> predicate, int msTimeout = 10000, int msDelay = 5)
=> WaitToAsync(predicate, msTimeout, msDelay).GetAwaiter().GetResult();
public static Task<bool> WaitToAsync(Func<bool> predicate, int msTimeout = 10000, int msDelay = 5)
{
return ToThreadPoolTask(async () =>
{
var result = false;
var dtTimeout = DateTime.UtcNow.AddMilliseconds(msTimeout).Ticks;
while (dtTimeout > DateTime.UtcNow.Ticks && !(result = predicate()))
await Task.Delay(msDelay).ConfigureAwait(false); //Thread.Sleep(msDelay);
return result;
});
}
public static void Forget(this Task task)
{
if (!task.IsCompleted || task.IsFaulted)
_ = ForgetAwaited(task);
static async Task ForgetAwaited(Task task)
{
try
{
await task.ConfigureAwait(false);
}
catch (Exception ex)
{
await Task.FromException(ex).ConfigureAwait(true);
}
}
}
//public static void Forget(this ValueTask task)
//{
// if (!task.IsCompleted || task.IsFaulted)
// _ = ForgetAwaited(task);
// static async ValueTask ForgetAwaited(ValueTask task)
// {
// try
// {
// await task.ConfigureAwait(false);
// }
// catch (Exception ex)
// {
// //TODO: .net5, .net6 feature! - J.
// ValueTask.FromException(ex).ConfigureAwait(true);
// }
// }
//}
//TODO: Cancellation token params - J.
//public static void RunOnThreadPool(this Task task) => Task.Run(() => _ = task).Forget(); //TODO: Letesztelni, a ThreadId-kat! - J.
public static void RunOnThreadPool(this Action action) => ToThreadPoolTask(action).Forget();
public static void RunOnThreadPool<T>(this Func<T> func) => ToThreadPoolTask(func).Forget();
public static Task ToThreadPoolTask(this Action action) => Task.Run(action);
//public static Task ToThreadPoolTask(this Task task) => Task.Run(() => _ = task);
//public static void ToParallelTaskStart(this Task task) => task.Start();
//public static Task<T> ToThreadPoolTask<T>(this Task<T> task) => Task.Run(() => task);
public static Task<T> ToThreadPoolTask<T>(this Func<Task<T>> func) => Task.Run(func); //TODO: Letesztelni, a ThreadId-kat! - J.
public static Task<T> ToThreadPoolTask<T>(this Func<T> func) => Task.Run(func);
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AyCode.Core.Consts;
namespace AyCode.Core.Logger
{
public static class Logger
{
public static void Info(string text)
{
Console.WriteLine(text);
}
public static void Warning(string text)
{
Console.WriteLine(text);
}
public static void Error(string text)
{
Console.WriteLine(text);
}
public static void Error(string text, Exception ex)
{
Console.WriteLine($"{text}{AcEnv.NL}{ex.Message}{AcEnv.NL}{ex}");
}
}
}

View File

@ -30,4 +30,8 @@
<ProjectReference Include="..\AyCode.Interfaces\AyCode.Interfaces.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="UserTokens\" />
</ItemGroup>
</Project>

View File

@ -1,10 +1,10 @@
using AyCode.Core.Tests;
using AyCode.Database.DataLayers;
using AyCode.Database.DbContexts;
namespace AyCode.Database.Tests;
//public abstract class DatabaseTestModelBase<TDal> : TestModelBase where TDal : DbContextBase
public abstract class DatabaseTestModelBase : TestModelBase
public abstract class DatabaseTestModelBase<TDbContext> : TestModelBase where TDbContext : DbContextBase
{
protected DatabaseTestModelBase()
{

View File

@ -1,7 +1,9 @@
using AyCode.Database.DbContexts;
namespace AyCode.Database.Tests
{
[TestClass]
public abstract class DatabaseTests : DatabaseTestModelBase
public abstract class DatabaseTests : DatabaseTestModelBase<DbContextBase>
{
[TestMethod]
public void TestMethod1()

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AyCode.Database.DbContexts;
using AyCode.Database.DbContexts.Users;
using AyCode.Interfaces.Users;
namespace AyCode.Database.Tests.Users
{
[TestClass]
public abstract class UserDbSetTest<TDbContext, TUser, TUserToken> : DatabaseTestModelBase<TDbContext>
where TDbContext : DbContextBase, IUserDbContextBase<TUser, TUserToken>
where TUser : class, IUserBase
where TUserToken : class, IUserTokenBase
{
//UserDalBase<UserDbContext, User, UserTokenBase>
//private UserDal _userDal;
[TestInitialize]
public void Setup()
{
}
[TestCleanup]
public void TearDown()
{
}
//[TestMethod]
//[DataRow("test@tiam.hu")]
//public void GetUserByEmail_ReturnsUser_WhenUserExists(string email)
//{
// //var userDal = PooledDal.CreateDal<UserDal>();
// var user = _userDal.GetUserByEmail(email);
// Assert.IsNotNull(user);
// Assert.AreEqual(email, user.EmailAddress);
//}
}
}

View File

@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AyCode.Database.DbContexts;
using AyCode.Utils.Extensions;
namespace AyCode.Database;
public class DalBase<TDbContext> where TDbContext : DbContextBase
{
public string Name { get; private set; }
private TDbContext _ctx;
public TDbContext Ctx => _ctx;
public DalBase() : this(Activator.CreateInstance<TDbContext>())
{ }
public DalBase(TDbContext ctx)
{
_ctx = ctx;
if (_ctx.Name.IsNullOrWhiteSpace())
_ctx.Name = _ctx.GetType().Name;
Name = $"{this.GetType().Name}, {_ctx.Name}";
}
public override string ToString()
{
return Name;
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AyCode.Core.Helpers;
using AyCode.Database.DbContexts;
using AyCode.Database.Extensions;
using AyCode.Interfaces.Entities;
using AyCode.Utils.Extensions;
using Microsoft.EntityFrameworkCore;
namespace AyCode.Database.DataLayers;
public abstract class DalBase<TDbContext> : IDalBase<TDbContext> where TDbContext : DbContextBase
{
public string Name { get; private set; }
public bool AutoCloseSession { get; set; }
public TDbContext Context { get; private set; }
//protected DalBase()
//{
// Name = $"{GetType().Name}";
//}
protected DalBase() : this(Activator.CreateInstance<TDbContext>())
{ }
protected DalBase(TDbContext ctx)
{
Context = ctx;
if (Context.Name.IsNullOrWhiteSpace())
Context.Name = Context.GetType().Name;
Name = $"{GetType().Name}, {Context.Name}";
}
public TDbContext CreateDbContext()
{
return Activator.CreateInstance<TDbContext>();
}
public void CloseDbContext(ref TDbContext? ctx)
{
ctx?.Dispose();
ctx = null;
}
public Task<TResultType> SessionAsync<TResultType>(Func<TDbContext, TResultType> callback)
=> this.SessionAsync<TDbContext, TResultType>(callback);
public Task<IEnumerable<TEntity>> SessionAsync<TEntity>(Func<TDbContext, IQueryable<TEntity>> callback) where TEntity : IEntity
=> this.SessionAsync<TDbContext, TEntity>(callback);
public TResultType Session<TResultType>(Func<TDbContext, TResultType> callback)
=> this.Session<TDbContext, TResultType>(callback);
public IEnumerable<TEntity> Session<TEntity>(Func<TDbContext, IQueryable<TEntity>> callback) where TEntity : IEntity
=> this.Session<TDbContext, TEntity>(callback);
public Task<bool> TransactionAsync(Func<TDbContext, bool> callbackTransactionBody, bool throwException = false)
=> this.TransactionAsync<TDbContext>(callbackTransactionBody, throwException);
public bool Transaction(Func<TDbContext, bool> callbackTransactionBody, bool throwException = false)
=> this.Transaction<TDbContext>(callbackTransactionBody, throwException);
public override string ToString()
{
return Name;
}
//public void Dispose()
//{
// Ctx?.Dispose();
// Ctx = null;
// //CloseDbContext(ref Ctx);
//}
}

View File

@ -0,0 +1,17 @@
using AyCode.Database.DbContexts;
namespace AyCode.Database.DataLayers;
public interface IDalBase //: IDisposable
{
public string Name { get; }
}
public interface IDalBase<TDbContext> : IDalBase where TDbContext : DbContextBase
{
public bool AutoCloseSession { get; set; }
public TDbContext Context { get; }
public TDbContext CreateDbContext();
public void CloseDbContext(ref TDbContext? ctx);
}

View File

@ -0,0 +1,64 @@
using System.Collections.Concurrent;
using AyCode.Utils.Extensions;
namespace AyCode.Database.DataLayers
{
public sealed class PooledDal
{
private static readonly PooledDal Instance = new();
private readonly ConcurrentDictionary<Guid, ConcurrentDictionary<Type, IDalBase>> _dataLayersPoolById = new();
private static ConcurrentDictionary<Guid, ConcurrentDictionary<Type, IDalBase>> DataLayersPoolById => Instance._dataLayersPoolById;
public static int Count => DataLayersPoolById.Count;
public static void Clear() => DataLayersPoolById.Clear();
public static TDal CreateDal<TDal>() where TDal : IDalBase => Activator.CreateInstance<TDal>();
/// <summary>
///
/// </summary>
/// <typeparam name="TDal"></typeparam>
/// <param name="id">ONLY SessionId or logged in PlayerId!</param>
/// <returns></returns>
public static TDal GetDalById<TDal>(Guid id) where TDal : IDalBase
{
if (id.IsNullOrEmpty()) return CreateDal<TDal>(); //TODO: A Guid.Empty id-t ÁTGONDOLNI! - J.
if (DataLayersPoolById.TryGetValue(id, out var dataLayersByType))
return GetDalByType<TDal>(dataLayersByType);
dataLayersByType = new ConcurrentDictionary<Type, IDalBase>();
DataLayersPoolById.TryAdd(id, dataLayersByType);
return GetDalByType<TDal>(dataLayersByType);
}
private static TDal GetDalByType<TDal>(ConcurrentDictionary<Type, IDalBase> dataLayersByType) where TDal : IDalBase
{
if (dataLayersByType.TryGetValue(typeof(TDal), out var dataLayer))
return (TDal)dataLayer;
var resultDal = CreateDal<TDal>();
dataLayersByType.TryAdd(typeof(TDal), resultDal);
return resultDal;
}
/// <summary>
///
/// </summary>
/// <param name="id">SessionId or PlayerId</param>
/// <returns></returns>
public static bool Remove(Guid id)//, bool keepGuidEmptyId = true)
{
//if (keepGuidEmptyId && id == Guid.Empty) return false;
if (!DataLayersPoolById.TryRemove(id, out var dataLayersByType))
return false;
dataLayersByType.Clear();
return true;
}
}
}

View File

@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AyCode.Database.DbContexts;
using AyCode.Database.DbContexts.Users;
using AyCode.Entities.Users;
using AyCode.Interfaces.Users;
using Microsoft.EntityFrameworkCore;
using AyCode.Database.Extensions;
using AyCode.Core.Consts;
namespace AyCode.Database.DataLayers.Users
{
public abstract class UserDalBase<TDbContext, TUser, TUserToken> : DalBase<TDbContext>, IUserDbContextBase<TUser, TUserToken>
where TDbContext : DbContextBase, IUserDbContextBase<TUser, TUserToken>
where TUser : class, IUserBase
where TUserToken : class, IUserTokenBase
{
public DbSet<TUser> Users { get; set; }
public DbSet<TUserToken> UserTokens { get; set; }
public TUser? GetUserById(Guid userId) => Session(x => x.GetUserById(userId));
public Task<TUser?> GetUserByIdAsync(Guid userId) => SessionAsync(x => x.GetUserById(userId));
public TUser? GetUserByEmail(string email) => Session(x => x.GetUserByEmail(email));
public Task<TUser?> GetUserByEmailAsync(string email) => SessionAsync(x => x.GetUserByEmail(email));
//public UserToken CreateUserToken(Guid userId, string verificationToken)
//{
// UserToken userToken = null;
// Transaction(ctx =>
// {
// userToken = ctx.CreateUserToken(userId, verificationToken);
// return userToken != null;
// }
// );
// return userToken;
//}
//public UserToken UpdateUserTokenSent(Guid userId, string verificationToken, DateTime tokenSentUtc)
//{
// UserToken userToken = null;
// Transaction(ctx =>
// {
// userToken = ctx.UpdateUserTokenSent(userId, verificationToken, tokenSentUtc);
// return userToken != null;
// }
// );
// return userToken;
//}
//public UserToken GetActiveUserToken(Guid userId)
// => Session(ctx => ctx.GetActiveUserToken(userId));
//public UserToken GetUserToken(Guid userId, string verificationToken)
// => Session(ctx => ctx.GetUserToken(userId, verificationToken));
//public bool IsValidToken(Guid userId, string verificationToken)
// => Transaction(ctx => ctx.IsValidToken(userId, verificationToken));
//public bool IsValidToken(string email, string verificationToken)
//{
// return Transaction(ctx =>
// {
// var user = ctx.GetUserByEmail(email);
// return user != null && ctx.IsValidToken(user.Id, verificationToken);
// });
//}
//public void DeactivateTokens(Guid userId)
//{
// Transaction(ctx =>
// {
// ctx.DeactivateTokens(userId);
// return true;
// });
//}
//public string GetValidVerificationToken(UserDal userDal, Guid userId, out UserToken userToken)
//{
// string verificationToken;
// userToken = userDal.GetActiveUserToken(userId);
// if (userToken == null)
// {
// verificationToken = AntGenerator.NewToken();
// userToken = userDal.CreateUserToken(userId, verificationToken);
// }
// else verificationToken = userToken.Token;
// return verificationToken;
//}
public AcErrorCode GetUserForEmailValidation(string email, Func<TUser?, AcErrorCode, AcErrorCode> callback)
{
TUser? user = null;
var errorCodes = AcErrorCode.Unset;
Transaction(ctx =>
{
var lowerEmail= email.ToLower();
user = ctx.Users.FirstOrDefault(x => x.EmailAddress == lowerEmail && x.EmailConfirmed);
if (user == null)
{
errorCodes = AcErrorCode.UserNameNotFound;
return false;
}
//user.Profile = ctx.Profiles.FirstOrDefault(x => x.OwnerId == user.Id);
//if (!ctx.IsUserConsentsAccepted(user.Id)) errorCodes = AcErrorCode.ConsentNotAccepted;
return true;
});
//if (user != null)
return callback(user, errorCodes);
}
}
}

View File

@ -0,0 +1,212 @@
using System.Diagnostics;
using AyCode.Core;
using AyCode.Database.DbContexts;
using AyCode.Database.DbContexts.Users;
using AyCode.Database.DbSets.Users;
using AyCode.Interfaces.Users;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace AyCode.Database.DataLayers.Users
{
public static class UserDalExtension
{
//public static bool DeleteUser(this IUserDbContextBase ctx, IUserBase user)
//{
// if (user == null) return false;
// var players = ctx.Players.Where(x => x.OwnerId == user.Id).ToList();
// foreach (var player in players)
// {
// ctx.Profiles.RemoveRange(ctx.Profiles.Where(x => x.OwnerId == player.PlayerId || x.OwnerId == user.Id));
// ctx.Wallets.RemoveRange(ctx.Wallets.Where(x => x.OwnerId == player.PlayerId || x.OwnerId == user.Id));
// ctx.Sessions.RemoveRange(ctx.Sessions.Where(x => x.PlayerId == player.PlayerId));
// //ctx.SessionDetails.RemoveRange(ctx.SessionDetails.Where(x => x.PlayerId == player.PlayerId));
// ctx.PlayerPermissionTags.RemoveRange(player.PermissionTags);
// }
// ctx.Players.RemoveRange(players);
// ctx.UserTokens.RemoveRange(ctx.UserTokens.Where(x => x.UserId == user.Id));
// //ctx.PermissionCategory.RemoveRange(ctx.PermissionCategory.Where(x => x.ForeignKey == user.Id));
// ctx.SaveChanges(); //A Player constraint miatt kell, különben nem engedi törölni a User-t! - J.
// return ctx.Remove(user).State == EntityState.Deleted;
//}
public static TUser? GetUserById<TUser>(this IUserDbSet<TUser> ctx, Guid userId) where TUser : class, IUserBase
{
return ctx.Users.FirstOrDefault(u => u.Id == userId);
}
public static TUser? GetUserByEmail<TUser>(this IUserDbSet<TUser> ctx, string email) where TUser : class, IUserBase
{
var emailLower = email.ToLower();
return ctx.Users.FirstOrDefault(u => u.EmailAddress == emailLower);
}
//public static IEnumerable<UserToUserConsent> GetUserConsentsByUserId(this UserDbContextBase ctx, Guid userId)
// => ctx.UserToUserConsents.Where(u => u.UserId == userId);
//public static bool IsUserConsentsAccepted(this UserDbContextBase ctx, Guid userId)
// => ctx.GetUserConsentsByUserId(userId).Count(x => x.IsAccepted) == AcConst.ConsentCount;
//public static bool SetUserConsentsToAccepted(this UserDbContextBase ctx, Guid userId)
//{
// var user = ctx.GetUserById(userId);
// return ctx.SetUserConsentsToAccepted(user);
//}
//public static bool SetUserConsentsToAccepted(this UserDbContextBase ctx, IUserBase? user)
//{
// if (!user.EmailConfirmed) return false; //Ez nem kéne ide... - J.
// var hashSet = ctx.GetUserConsentsByUserId(user.Id).Select(x => x.UserConsentId).ToHashSet();
// var userConsents = ctx.UserConsents.Where(x => !hashSet.Contains(x.Id));
// ctx.AddRange(userConsents.Select(userConsent => new UserToUserConsent(user.Id, userConsent.Id, true)));
// var userToUserConsents = ctx.GetUserConsentsByUserId(user.Id).Where(x => !x.IsAccepted).ToList();
// foreach (var userToUserConsent in userToUserConsents)
// {
// userToUserConsent.IsAccepted = true;
// }
// ctx.UpdateRange(userToUserConsents);
// return true;
//}
public static bool ChangePassword<TUser, TUserToken>(this IUserDbContextBase<TUser, TUserToken> ctx, TUser? user, string passwordHash, string verificationToken)
where TUser : class, IUserBase
where TUserToken : class, IUserTokenBase
{
if (user == null || !ctx.IsValidToken(user.Id, verificationToken)) return false;
user.Password = passwordHash;
ctx.Users.Update(user);
ctx.DeactivateTokens(user.Id);
return true;
}
public static TUserToken? UpdateUserTokenSent<TUserToken>(this IUserTokenDbSet<TUserToken> ctx, Guid userId, string verificationToken, DateTime tokenSentUtc) where TUserToken : class, IUserTokenBase
{
var userToken = ctx.GetUserToken(userId, verificationToken);
if (userToken == null) return null;
userToken.TokenSent = tokenSentUtc;
userToken.TokenExpiration ??= tokenSentUtc.AddDays(1);
var utcNow = DateTime.UtcNow;
userToken.IsActive = tokenSentUtc <= utcNow && userToken.TokenExpiration > utcNow;
ctx.UserTokens.Update(userToken);
return userToken;
}
public static TUserToken CreateUserToken<TUserToken>(this IUserTokenDbSet<TUserToken> ctx, Guid userId, string verificationToken) where TUserToken : class, IUserTokenBase
{
var userToken = Activator.CreateInstance<TUserToken>();
userToken.UserId = userId;
userToken.Token = verificationToken;
return ctx.CreateUserToken(userToken);
}
public static TUserToken CreateUserToken<TUserToken>(this IUserTokenDbSet<TUserToken> ctx, TUserToken userToken) where TUserToken : class, IUserTokenBase
{
ctx.DeactivateTokens(userToken.UserId);
userToken.IsActive = true;
ctx.UserTokens.Add(userToken);
return userToken;
}
public static TUserToken? GetActiveUserToken<TUserToken>(this IUserTokenDbSet<TUserToken> ctx, Guid userId) where TUserToken : class, IUserTokenBase
=> ctx.UserTokens.SingleOrDefault(x => x.UserId == userId && x.IsActive && (x.TokenExpiration == null || x.TokenExpiration > DateTime.UtcNow));
public static TUserToken? GetUserToken<TUserToken>(this IUserTokenDbSet<TUserToken> ctx, Guid userId, string verificationToken) where TUserToken : class?, IUserTokenBase?
=> ctx.UserTokens.SingleOrDefault(x => x.UserId == userId && x.Token == verificationToken);
public static bool IsValidToken<TUserToken>(this IUserTokenDbSet<TUserToken> ctx, Guid userId, string verificationToken) where TUserToken : class, IUserTokenBase
=> ctx.UserTokens.Any(x => x.UserId == userId && x.IsActive && x.Token == verificationToken && x.TokenExpiration > DateTime.UtcNow && x.TokenSent < DateTime.UtcNow);
public static void DeactivateTokens<TUserToken>(this IUserTokenDbSet<TUserToken> ctx, Guid userId) where TUserToken : class, IUserTokenBase
{
var activeUserTokens = ctx.UserTokens.Where(x => x.UserId == userId && x.IsActive);
foreach (var activeUserToken in activeUserTokens)
activeUserToken.IsActive = false;
ctx.UserTokens.UpdateRange(activeUserTokens);
}
////public static void GetUserDetails(this IUserDal userDal, Guid userId, Action<AspNetUser> callback)
//// => callback(userDal.Get<AspNetUser>(userId));
//public static void UsernameAndEmailAvailable(this IUserDal userDal, string username, string email, Action<bool, AcErrorCode, AspNetUser> callback)
//{
// IUserBase notConfirmedUser = null;
// var lowerEmail = email.ToLower();
// var lowerUserName = username.ToLower();
// var isAvailable = AcValidate.IsValidEmailFormat(lowerEmail, out var resultErrorCode) && AcValidate.IsValidUserNameFormat(lowerUserName, out resultErrorCode);
// if (isAvailable)
// {
// //userDal.RowCount<AspNetUser>(ctx => ctx.EmailConfirmed && (ctx.NormalizedUserName == lowerUserName || ctx.NormalizedEmail == lowerEmail)) == 0
// var users = userDal.Where<AspNetUser>(ctx => ctx.NormalizedUserName == lowerUserName || ctx.NormalizedEmail == lowerEmail).ToList();
// notConfirmedUser = users.FirstOrDefault(x => !x.EmailConfirmed && x.NormalizedEmail == lowerEmail);
// //isAvailable = (users.Count == 0 || (users.Count == 1 && notConfirmedUser != null));
// if (users.Count > 1) resultErrorCode = AcErrorCode.EmailInUse;
// else if (users.Count == 1 && notConfirmedUser == null)
// {
// resultErrorCode = users.Any(x => x.NormalizedEmail == lowerEmail) ? AcErrorCode.EmailInUse : AcErrorCode.UserNameInUse;
// }
// isAvailable = resultErrorCode == AcErrorCode.Unset;
// }
// callback(isAvailable, resultErrorCode, notConfirmedUser);
//}
//public static void AddNewUser(this IUserDal userDal, IUserBase newUser, Action<AspNetUser> callback)
//{
// userDal.Add(newUser);
// callback(newUser);
//}
//public static void AddNewUser(this IUserDal userDal, Guid userId, string username, string email, string password, Action callback)
//{
// var newUser = new AspNetUser(userId, username, email, password);
// userDal.Add(newUser);
// callback();
//}
////public static void DeleteUser(this IUserDal userDal, string username, Action callback)
////{
//// userDal.Session(ctx =>
//// {
//// var user = ctx.Users.FirstOrDefault(u => u.NormalizedUserName == username.ToUpperInvariant());
//// if (user == null) return;
//// ctx.Set<AspNetUser>().Delete(user);
//// ctx.SaveChanges();
//// };
////}
}
}

View File

@ -0,0 +1,9 @@
using AyCode.Database.DbSets.Users;
using AyCode.Interfaces.Users;
using Microsoft.EntityFrameworkCore;
namespace AyCode.Database.DbContexts.Users;
public interface IUserDbContextBase<TUser, TUserToken> : IUserDbSet<TUser>, IUserTokenDbSet<TUserToken> where TUser : class, IUserBase where TUserToken : class, IUserTokenBase
{
}

View File

@ -0,0 +1,9 @@
using AyCode.Interfaces.Users;
using Microsoft.EntityFrameworkCore;
namespace AyCode.Database.DbContexts.Users;
public interface IUserTokenDbContextBase<TUserToken> where TUserToken : class, IUserTokenBase
{
DbSet<TUserToken> UserToken { get; set; }
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AyCode.Interfaces.Users;
using Microsoft.EntityFrameworkCore;
namespace AyCode.Database.DbContexts.Users
{
public abstract class UserDbContextBase<TUser, TUserToken> : DbContextBase, IUserDbContextBase<TUser, TUserToken> where TUser : class, IUserBase where TUserToken : class, IUserTokenBase
{
public required DbSet<TUser> Users { get; set; }
public required DbSet<TUserToken> UserTokens { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using AyCode.Database.DbContexts;
using AyCode.Interfaces.Users;
using Microsoft.EntityFrameworkCore;
namespace AyCode.Database.DbSets.Users;
public interface IUserDbSet<TUser> where TUser : class, IUserBase
{
DbSet<TUser> Users { get; set; }
}

View File

@ -0,0 +1,9 @@
using AyCode.Interfaces.Users;
using Microsoft.EntityFrameworkCore;
namespace AyCode.Database.DbSets.Users;
public interface IUserTokenDbSet<TUserToken> where TUserToken : class, IUserTokenBase
{
DbSet<TUserToken> UserTokens { get; set; }
}

View File

@ -0,0 +1,39 @@
using AyCode.Core.Helpers;
using AyCode.Database.DataLayers;
using AyCode.Database.DbContexts;
using AyCode.Interfaces.Entities;
namespace AyCode.Database.Extensions;
public static class DalExtension
{
public static Task<TResultType> SessionAsync<TDbContext, TResultType>(this IDalBase<TDbContext> dal, Func<TDbContext, TResultType> callback) where TDbContext : DbContextBase
=> TaskHelper.ToThreadPoolTask(() => dal.Session(callback));
public static Task<IEnumerable<TEntity>> SessionAsync<TDbContext, TEntity>(this IDalBase<TDbContext> dal, Func<TDbContext, IQueryable<TEntity>> callback) where TEntity : IEntity where TDbContext : DbContextBase
=> TaskHelper.ToThreadPoolTask(() => dal.Session(callback));
public static TResultType Session<TDbContext, TResultType>(this IDalBase<TDbContext> dal, Func<TDbContext, TResultType> callback) where TDbContext : DbContextBase
{
using var ctx = dal.CreateDbContext();
return ctx.Session(callback);
}
public static IEnumerable<TEntity> Session<TDbContext, TEntity>(this IDalBase<TDbContext> dal, Func<TDbContext, IQueryable<TEntity>> callback) where TEntity : IEntity where TDbContext : DbContextBase
{
using var ctx = dal.CreateDbContext();
return ctx.Session(callback);
}
public static Task<bool> TransactionAsync<TDbContext>(this IDalBase<TDbContext> dal, Func<TDbContext, bool> callbackTransactionBody, bool throwException = false) where TDbContext : DbContextBase
=> TaskHelper.ToThreadPoolTask(() => dal.Transaction(callbackTransactionBody, throwException));
public static bool Transaction<TDbContext>(this IDalBase<TDbContext> dal, Func<TDbContext, bool> callbackTransactionBody, bool throwException = false) where TDbContext : DbContextBase
{
using var ctx = dal.CreateDbContext();
return ctx.Transaction(callbackTransactionBody, throwException);
}
}

View File

@ -0,0 +1,41 @@
using AyCode.Core.Consts;
using AyCode.Core.Helpers;
using AyCode.Core.Logger;
using AyCode.Database.DbContexts;
using AyCode.Interfaces.Entities;
namespace AyCode.Database.Extensions;
public static class DbSessionExtension
{
public static Task<TResultType> SessionAsync<TDbContext, TResultType>(this TDbContext ctx, Func<TDbContext, TResultType> callback) where TDbContext : DbContextBase
=> TaskHelper.ToThreadPoolTask(() => ctx.Session(callback));
public static TResultType Session<TDbContext, TResultType>(this TDbContext ctx, Func<TDbContext, TResultType> callback) where TDbContext : DbContextBase
{
TResultType result;
try
{
result = callback(ctx);
}
catch (Exception ex)
{
var errorText = $"Session({ctx}) callback error...{AcEnv.NL}";
Logger.Error($"{errorText}", ex);
throw new Exception($"{errorText}{ex}");
}
return result;
}
public static IEnumerable<TEntity> Session<TDbContext, TEntity>(this TDbContext ctx, Func<TDbContext, IQueryable<TEntity>> callback)
where TEntity : IEntity
where TDbContext : DbContextBase
{
foreach (var entity in callback(ctx))
yield return entity;
}
}

View File

@ -0,0 +1,86 @@
using AyCode.Core.Helpers;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyCode.Database.DbContexts;
using AyCode.Core.Logger;
using AyCode.Core.Consts;
namespace AyCode.Database.Extensions;
public static class DbTransactionExtension
{
/// <summary>
/// BeginTransaction
/// </summary>
/// <returns></returns>
private static IDbContextTransaction OpenTransaction<TDbContext>(this TDbContext ctx) where TDbContext : DbContextBase
{
ctx.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never;
return ctx.Database.BeginTransaction(); //IDbContextTransaction
}
/// <summary>
///
/// </summary>
/// <returns>True, ha sikeres volt a Commit.</returns>
private static bool CommitTransaction<TDbContext>(this TDbContext ctx, bool throwException = false) where TDbContext : DbContextBase
{
var result = false;
var transaction = ctx.Database?.CurrentTransaction;
if (transaction == null) return false;
try
{
ctx.SaveChanges();
transaction.Commit();
result = true;
}
catch (Exception ex)
{
var errorText = $"Transaction({ctx}) transaction ROLLBACK!{AcEnv.NL}";
transaction.Rollback();
if (throwException) throw new Exception(errorText, ex);
Logger.Error($"{errorText}", ex);
}
return result;
}
public static Task<bool> TransactionAsync<TDbContext>(this TDbContext ctx, Func<TDbContext, bool> callbackTransactionBody, bool throwException = false) where TDbContext : DbContextBase
=> TaskHelper.ToThreadPoolTask(() => ctx.Transaction(callbackTransactionBody, throwException));
public static bool Transaction<TDbContext>(this TDbContext ctx, Func<TDbContext, bool> callbackTransactionBody, bool throwException = false) where TDbContext : DbContextBase
{
var result = false;
try
{
using var transaction = ctx.OpenTransaction();
if (callbackTransactionBody(ctx)) result = ctx.CommitTransaction(throwException);
else
{
transaction.Rollback();
Logger.Warning($"Transaction({ctx}) transaction ROLLBACK!");
}
}
catch (Exception ex)
{
if (throwException) throw;
result = false;
Logger.Error($"Transaction({ctx}) transaction error...{AcEnv.NL}", ex);
}
finally
{
//result = ctx.CloseDbContext(false) && result;
}
return result;
}
}

View File

@ -12,43 +12,41 @@ namespace AyCode.Entities.Users
[Table("Users")]
public class UserBase : IUserBase
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
[Column("Email")]
public string EmailAddress { get; set; }
//public string NormalizedEmail { get; set; }
public string PhoneNumber { get; set; }
public string Password { get; set; }
public string? RefreshToken { get; set; }
public bool EmailConfirmed { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
public UserBase() { }
public UserBase(string email, string password) : this(Guid.NewGuid(), email, password) { }
public UserBase(Guid id, string email, string password) : this()
{
Id = id;
Email = email;
EmailAddress = email;
Password = password;
}
public UserBase(string email, string phoneNumber, string password) : this(Guid.NewGuid(), email, phoneNumber, password) { }
public UserBase(Guid id, string email, string phoneNumber, string password) : this()
public UserBase(Guid id, string email, string phoneNumber, string password) : this(id, email, password)
{
Id = id;
Email = email;
PhoneNumber = phoneNumber;
Password = password;
}
public UserBase(string email, string phoneNumber, string password, string refreshToken) : this(Guid.NewGuid(), email, phoneNumber, password, refreshToken) { }
public UserBase(Guid id, string email, string phoneNumber, string password, string refreshToken) : this()
public UserBase(Guid id, string email, string phoneNumber, string password, string refreshToken) : this(id, email, phoneNumber, password)
{
Id = id;
Email = email;
PhoneNumber = phoneNumber;
Password = password;
RefreshToken = refreshToken;
}
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public string Password { get; set; }
public string? RefreshToken { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using AyCode.Interfaces.Users;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace AyCode.Entities.Users;
[Table("UserToken")]
public class UserTokenBase : IUserTokenBase
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required] public Guid UserId { get; set; }
[Required] public bool IsActive { get; set; }
[Required, MaxLength(32)]
public string Token { get; set; }
public DateTime? TokenSent { get; set; }
public DateTime? TokenExpiration { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace AyCode.Interfaces.Users;
public interface IEmailAddress
{
[MaxLength(150)]
string EmailAddress { get; set; }
bool EmailConfirmed { get; set; }
//[MaxLength(150)]
//string NormalizedEmail { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace AyCode.Interfaces.Users;
public interface IPassword
{
string Password { get; set; }
}

View File

@ -4,8 +4,6 @@ using AyCode.Interfaces.TimeStampInfo;
namespace AyCode.Interfaces.Users;
public interface IUserBase : IEntityGuid, ITimeStampInfo
public interface IUserBase : IEntityGuid, IEmailAddress, IPassword, ITimeStampInfo
{
string Email { get; }
string Password { get; }
}

View File

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using AyCode.Interfaces.Entities;
using AyCode.Interfaces.TimeStampInfo;
namespace AyCode.Interfaces.Users;
public interface IUserTokenBase : IEntity<int>, ITimeStampInfo
{
public Guid UserId { get; set; }
public bool IsActive { get; set; }
public string Token { get; set; }
public DateTime? TokenSent { get; set; }
public DateTime? TokenExpiration { get; set; }
}