diff --git a/AyCode.Core.Tests/AyCode.Core.Tests.csproj b/AyCode.Core.Tests/AyCode.Core.Tests.csproj index 471fb92..2c5868e 100644 --- a/AyCode.Core.Tests/AyCode.Core.Tests.csproj +++ b/AyCode.Core.Tests/AyCode.Core.Tests.csproj @@ -11,8 +11,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/AyCode.Core.sln b/AyCode.Core.sln index 3ed7ba3..7ef3cbf 100644 --- a/AyCode.Core.sln +++ b/AyCode.Core.sln @@ -25,13 +25,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AyCode.Database.Tests", "Ay EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AyCode.Core.Tests", "AyCode.Core.Tests\AyCode.Core.Tests.csproj", "{320A245F-6731-476D-A9D8-77888E6B5D9C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Models", "AyCode.Models\AyCode.Models.csproj", "{21392620-7D0E-44B6-9485-93C57F944C20}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AyCode.Models", "AyCode.Models\AyCode.Models.csproj", "{21392620-7D0E-44B6-9485-93C57F944C20}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Services", "AyCode.Services\AyCode.Services.csproj", "{58C8A6A7-D624-4E32-93B9-16B879405CAA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AyCode.Services", "AyCode.Services\AyCode.Services.csproj", "{58C8A6A7-D624-4E32-93B9-16B879405CAA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Models.Server", "AyCode.Models.Server\AyCode.Models.Server.csproj", "{44CF90C8-76E4-4BD6-A957-E8F7AE019B06}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AyCode.Models.Server", "AyCode.Models.Server\AyCode.Models.Server.csproj", "{44CF90C8-76E4-4BD6-A957-E8F7AE019B06}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Services.Server", "AyCode.Services.Server\AyCode.Services.Server.csproj", "{3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AyCode.Services.Server", "AyCode.Services.Server\AyCode.Services.Server.csproj", "{3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Services.Server.Tests", "AyCode.Services.Server.Tests\AyCode.Services.Server.Tests.csproj", "{9AC9AF60-280A-4871-A7FA-69AB4D0C858A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -99,6 +101,10 @@ Global {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C74C94F-2FEB-47F7-ABB3-B0C9CBCCC876}.Release|Any CPU.Build.0 = Release|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AC9AF60-280A-4871-A7FA-69AB4D0C858A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AyCode.Core/AyCode.Core.csproj b/AyCode.Core/AyCode.Core.csproj index 46fad1b..213344f 100644 --- a/AyCode.Core/AyCode.Core.csproj +++ b/AyCode.Core/AyCode.Core.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/AyCode.Core/Consts/AcConst.cs b/AyCode.Core/Consts/AcConst.cs index 2f9afd7..02c89ab 100644 --- a/AyCode.Core/Consts/AcConst.cs +++ b/AyCode.Core/Consts/AcConst.cs @@ -1,7 +1,15 @@ -namespace AyCode.Core.Consts +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using AyCode.Core.Extensions; + +namespace AyCode.Core.Consts { - public static class AcConst + public abstract class AcConst { + public static Guid ProjectId; + public static string ProjectSalt; + public const int ImageUploadLimit = 4096 * 1024; public static int ConsentCount = 1; @@ -47,6 +55,7 @@ /// MainScene, Lobby /// public static readonly Guid AnataCity; + public static readonly Guid PlayerGarage; public static readonly Guid PlayerWardrobe; public static readonly Guid LoadingId; @@ -56,6 +65,7 @@ /// RegionAdmin /// public static readonly Guid RegionAdminUserId; + public static readonly Guid RegionAdminPlayerId; public static readonly Guid GamePlayer; @@ -67,8 +77,13 @@ 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" }; + //TODO: kitörölni és DB-ből kiszedni! - J. + private static string _tiamProjectIdString = "684f34d1-163a-4077-918f-a9d9df5ce789"; static AcConst() { + ProjectId = Guid.Parse(_tiamProjectIdString); + ProjectSalt = GenerateProjectSalt(ProjectId.ToString("N")); + //var anataFolder = AcDomain.IsProductVersion ? "Anata" : "AnataDev"; //AnataRootCacheFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AyCode", anataFolder); @@ -100,5 +115,30 @@ //AnataWorldDomainId = Guid.Parse("8EFA53B3-7114-41E2-98F1-D690ECF509D2"); //AnataWorldBrandId = Guid.Parse("99265775-4B19-4975-B338-EB0F2F890806"); } + + //SEMMILYEN KÖRÜLMÉNYEK KÖZÖTT SE VÁLTOZTASD MEG METÓDUS LOGIKÁJÁT!!! Különben senki sem fog tudni Login-olni! - J. + public static string GenerateProjectSalt(string projectIdString) + { + if (projectIdString.IsNullOrWhiteSpace()) return string.Empty; + + var projectSalt = new StringBuilder(); + + var projectIdChars = projectIdString.ToCharArray(); + var mid = (projectIdChars.Length + 1) / 2; + + var firstHalf = projectIdChars.Take(mid).Reverse(); + var secondHalf = projectIdChars.Skip(mid).Reverse(); + var mixedChars = firstHalf.MixCharacters(secondHalf).ToArray(); + + for (var i = mixedChars.Length - 1; i >= 0; i--) + { + projectSalt.Append(mixedChars[i]); + + mixedChars[i] = ' '; + projectIdChars[i] = ' '; + } + + return projectSalt.ToString(); + } } } diff --git a/AyCode.Core/Consts/AcErrorCode.cs b/AyCode.Core/Consts/AcErrorCode.cs index f8dda75..c414fa0 100644 --- a/AyCode.Core/Consts/AcErrorCode.cs +++ b/AyCode.Core/Consts/AcErrorCode.cs @@ -53,5 +53,6 @@ DomainIdNotFound = 43, DisableAddUser = 44, + PhoneNumberFormatIsNotValid = 45, } } diff --git a/AyCode.Core/Consts/AcRegExpression.cs b/AyCode.Core/Consts/AcRegExpression.cs new file mode 100644 index 0000000..c6a68a9 --- /dev/null +++ b/AyCode.Core/Consts/AcRegExpression.cs @@ -0,0 +1,15 @@ +using System.Text.RegularExpressions; + +namespace AyCode.Core.Consts; + +public static partial class AcRegExpression +{ + public const string EmailMask = @"(\w|[.-])+@(\w|-)+\.(\w|-){2,4}"; + public const string PhoneNumberMask = @"\+(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\W*\d\W*\d\W*\d\W*\d\W*\d\W*\d\W*\d\W*\d\W*(\d{1,2})"; + + [GeneratedRegex(AcRegExpression.EmailMask)] + public static partial Regex EmailRegex(); + + [GeneratedRegex(AcRegExpression.PhoneNumberMask)] + public static partial Regex PhoneNumberRegex(); +} \ No newline at end of file diff --git a/AyCode.Core/Consts/AcValidate.cs b/AyCode.Core/Consts/AcValidate.cs index cd69019..d3ba4ea 100644 --- a/AyCode.Core/Consts/AcValidate.cs +++ b/AyCode.Core/Consts/AcValidate.cs @@ -1,10 +1,16 @@ using AyCode.Utils.Extensions; using System.Text.RegularExpressions; +using AyCode.Core.Extensions; namespace AyCode.Core.Consts { public static class AcValidate { + public static bool IsValidEmailAndPasswordFormat(string email, string password, out AcErrorCode acErrorCode) + { + return IsValidEmailFormat(email, out acErrorCode) && IsValidPasswordFormat(password, out acErrorCode); + } + public static bool IsValidUserNameAndPasswordFormat(string username, string password, out AcErrorCode acErrorCode) { return IsValidUserNameFormat(username, out acErrorCode) && IsValidPasswordFormat(password, out acErrorCode); @@ -45,12 +51,23 @@ namespace AyCode.Core.Consts acErrorCode = AcErrorCode.Unset; //a@a.a -> length == 5 - var isValid = !email.IsNullOrWhiteSpace() && email.Length >= 5 && email.Contains('@') && email.Contains('.') && !email.Contains(' '); + //var isValid = !email.IsNullOrWhiteSpace() && email.Length >= 5 && email.Contains('@') && email.Contains('.') && !email.Contains(' '); + var isValid = AcRegExpression.EmailRegex().IsMatch(email); if (!isValid) acErrorCode = AcErrorCode.EmailFormatIsNotValid; return isValid; } + public static bool IsValidPhoneNumberFormat(string phoneNumber, out AcErrorCode acErrorCode) + { + acErrorCode = AcErrorCode.Unset; + + var isValid = AcRegExpression.PhoneNumberRegex().IsMatch(phoneNumber); + + if (!isValid) acErrorCode = AcErrorCode.PhoneNumberFormatIsNotValid; + return isValid; + } + public static bool IsValidUserTokenFormat(string verificationToken, out AcErrorCode acErrorCode) { acErrorCode = AcErrorCode.Unset; diff --git a/AyCode.Database.Tests/AyCode.Database.Tests.csproj b/AyCode.Database.Tests/AyCode.Database.Tests.csproj index b4336aa..db41b0b 100644 --- a/AyCode.Database.Tests/AyCode.Database.Tests.csproj +++ b/AyCode.Database.Tests/AyCode.Database.Tests.csproj @@ -11,8 +11,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/AyCode.Database/AyCode.Database.csproj b/AyCode.Database/AyCode.Database.csproj index be6b751..d6334d3 100644 --- a/AyCode.Database/AyCode.Database.csproj +++ b/AyCode.Database/AyCode.Database.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/AyCode.Database/DataLayers/AcDalBase.cs b/AyCode.Database/DataLayers/AcDalBase.cs index 4691581..d02bb95 100644 --- a/AyCode.Database/DataLayers/AcDalBase.cs +++ b/AyCode.Database/DataLayers/AcDalBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using AyCode.Core.Extensions; using AyCode.Core.Helpers; using AyCode.Database.DbContexts; using AyCode.Database.Extensions; diff --git a/AyCode.Database/DataLayers/Users/AcUserDalBase.cs b/AyCode.Database/DataLayers/Users/AcUserDalBase.cs index 64675fe..d58903c 100644 --- a/AyCode.Database/DataLayers/Users/AcUserDalBase.cs +++ b/AyCode.Database/DataLayers/Users/AcUserDalBase.cs @@ -32,40 +32,51 @@ namespace AyCode.Database.DataLayers.Users public TUser? GetUserByEmail(string email) => Session(x => x.GetUserByEmail(email)); public Task GetUserByEmailAsync(string email) => SessionAsync(x => x.GetUserByEmail(email)); - public Task AddUser(TUser user, string profileName, TProfileAddress address, string? firstName = null, string? lastName = null) + public Task AddUserAsync(TUser user) + { + return TransactionAsync(ctx => ctx.AddUser(user)); + } + + public Task AddUserAsync(TUser user, string profileName, TProfileAddress address, string? firstName = null, string? lastName = null) { return TransactionAsync(ctx => { - var profile = Activator.CreateInstance(); - - profile.Id = Guid.NewGuid(); - profile.Name = profileName; - profile.FirstName = firstName; - profile.LastName = lastName; - profile.Address = address; - + var profile = CreateProfile(profileName, address, firstName, lastName); user.Profile= profile; - if (ctx.AddUser(user)) return false; - - ctx.SaveChanges(); - - return true; + return ctx.AddUser(user); }); } - public Task AddUser(TUser user) + public bool AddUser(TUser user, string profileName, TProfileAddress address, string? firstName = null, string? lastName = null) { - return TransactionAsync(ctx => + return Transaction(ctx => { - if (ctx.AddUser(user)) return false; - - ctx.SaveChanges(); + var profile = CreateProfile(profileName, address, firstName, lastName); - return true; + user.Profile = profile; + + return ctx.AddUser(user); }); } + private static TProfile CreateProfile(string profileName, TProfileAddress address, string? firstName, string? lastName) + { + var profile = Activator.CreateInstance(); + + profile.Id = Guid.NewGuid(); + profile.Name = profileName; + profile.FirstName = firstName; + profile.LastName = lastName; + + profile.Address = address; + profile.AddressId = address.Id; + return profile; + } + + public Task RemoveUserAsync(TUser user) => TransactionAsync(ctx => ctx.RemoveUserAsync(user)); + public Task RemoveUserAsync(Guid userId) => TransactionAsync(ctx => ctx.RemoveUserAsync(userId)); + //public UserToken CreateUserToken(Guid userId, string verificationToken) //{ // UserToken userToken = null; diff --git a/AyCode.Entities/Addresses/AcAddress.cs b/AyCode.Entities/Addresses/AcAddress.cs index 852c28e..27e871e 100644 --- a/AyCode.Entities/Addresses/AcAddress.cs +++ b/AyCode.Entities/Addresses/AcAddress.cs @@ -13,8 +13,8 @@ public abstract class AcAddress : IAcAddress public bool IsValid { get; set; } public bool IsHelper { get; set; } - public double Latitude { get; set; } - public double Longitude { get; set; } + public double? Latitude { get; set; } + public double? Longitude { get; set; } public string? AddressText { get; set; } diff --git a/AyCode.Entities/Profiles/AcProfile.cs b/AyCode.Entities/Profiles/AcProfile.cs index 138b1c6..8ab4b23 100644 --- a/AyCode.Entities/Profiles/AcProfile.cs +++ b/AyCode.Entities/Profiles/AcProfile.cs @@ -1,6 +1,7 @@ using AyCode.Interfaces.Profiles; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using AyCode.Core.Extensions; using AyCode.Interfaces.Addresses; using AyCode.Utils.Extensions; diff --git a/AyCode.Entities/Users/AcUser.cs b/AyCode.Entities/Users/AcUser.cs index 515d252..a71e947 100644 --- a/AyCode.Entities/Users/AcUser.cs +++ b/AyCode.Entities/Users/AcUser.cs @@ -23,7 +23,7 @@ namespace AyCode.Entities.Users [Required, Column("Email")] public string EmailAddress { get; set; } //public string NormalizedEmail { get; set; } - public string PhoneNumber { get; set; } + public string? PhoneNumber { get; set; } public string Password { get; set; } public string? RefreshToken { get; set; } diff --git a/AyCode.Interfaces.Server/AyCode.Interfaces.Server.csproj b/AyCode.Interfaces.Server/AyCode.Interfaces.Server.csproj index 30402ac..f7f8798 100644 --- a/AyCode.Interfaces.Server/AyCode.Interfaces.Server.csproj +++ b/AyCode.Interfaces.Server/AyCode.Interfaces.Server.csproj @@ -6,4 +6,16 @@ enable + + + + + + + + + + + + diff --git a/AyCode.Interfaces.Server/Logins/IAcLoginServiceServer.cs b/AyCode.Interfaces.Server/Logins/IAcLoginServiceServer.cs new file mode 100644 index 0000000..946545d --- /dev/null +++ b/AyCode.Interfaces.Server/Logins/IAcLoginServiceServer.cs @@ -0,0 +1,20 @@ +using System.Net; +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Logins; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.Users; + +namespace AyCode.Interfaces.Server.Logins; + +public interface IAcLoginServiceServer : IAcLoginServiceCommon + + where TUser : class, IAcUser + where TUserToken : class, IAcUserTokenBase + where TProfile : class, IAcProfile + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + where TProfileAddress : class, IAcAddress +{ + protected string GenerateDynamicSalt(Guid userId); +} \ No newline at end of file diff --git a/AyCode.Interfaces/Addresses/Dtos/IAcAddressDtoBase.cs b/AyCode.Interfaces/Addresses/Dtos/IAcAddressDtoBase.cs index 967d007..d4be114 100644 --- a/AyCode.Interfaces/Addresses/Dtos/IAcAddressDtoBase.cs +++ b/AyCode.Interfaces/Addresses/Dtos/IAcAddressDtoBase.cs @@ -8,7 +8,7 @@ public interface IAcAddressDtoBase : IEntityGuid public bool IsValid { get; set; } public bool IsHelper { get; set; } - public double Latitude { get; set; } - public double Longitude { get; set; } + public double? Latitude { get; set; } + public double? Longitude { get; set; } public string? AddressText { get; set; } } \ No newline at end of file diff --git a/AyCode.Interfaces/Logins/IAcLoginServiceBase.cs b/AyCode.Interfaces/Logins/IAcLoginServiceBase.cs new file mode 100644 index 0000000..dc7ab32 --- /dev/null +++ b/AyCode.Interfaces/Logins/IAcLoginServiceBase.cs @@ -0,0 +1,15 @@ +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.Users; + +namespace AyCode.Interfaces.Logins; + +public interface IAcLoginServiceBase where TUser : class, IAcUser + where TProfile : class, IAcProfile + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + where TProfileAddress : class, IAcAddress +{ + public TUser? LoggedInUser { get; } +} \ No newline at end of file diff --git a/AyCode.Interfaces/Logins/IAcLoginServiceClient.cs b/AyCode.Interfaces/Logins/IAcLoginServiceClient.cs new file mode 100644 index 0000000..a2e31ab --- /dev/null +++ b/AyCode.Interfaces/Logins/IAcLoginServiceClient.cs @@ -0,0 +1,15 @@ +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.Users; + +namespace AyCode.Interfaces.Logins; + +public interface IAcLoginServiceClient : IAcLoginServiceCommon + where TUser : class, IAcUser + where TProfile : class, IAcProfile + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + where TProfileAddress : class, IAcAddress +{ +} \ No newline at end of file diff --git a/AyCode.Interfaces/Logins/IAcLoginServiceCommon.cs b/AyCode.Interfaces/Logins/IAcLoginServiceCommon.cs new file mode 100644 index 0000000..ff889f0 --- /dev/null +++ b/AyCode.Interfaces/Logins/IAcLoginServiceCommon.cs @@ -0,0 +1,25 @@ +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.Users; + +namespace AyCode.Interfaces.Logins; + +public interface IAcLoginServiceCommon : IAcLoginServiceBase + where TUser : class, IAcUser + where TProfile : class, IAcProfile + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + where TProfileAddress : class, IAcAddress +{ + public TUser? Login(string email, string password, out string accessToken); + public Task LoginAsync(string email, string password); + + public bool Logout(); + public Task LogoutAsync(); + + public TUser? Registration(string email, string password, string? phoneNumber = null); + public TUser? Registration(Guid userId, string email, string password, string? phoneNumber = null); + public Task RegistrationAsync(string email, string password, string? phoneNumber = null); + public Task RegistrationAsync(Guid userId, string email, string password, string? phoneNumber = null); +} \ No newline at end of file diff --git a/AyCode.Interfaces/Users/IAcUserBase.cs b/AyCode.Interfaces/Users/IAcUserBase.cs index e50f92b..4cd9d96 100644 --- a/AyCode.Interfaces/Users/IAcUserBase.cs +++ b/AyCode.Interfaces/Users/IAcUserBase.cs @@ -6,7 +6,7 @@ namespace AyCode.Interfaces.Users; public interface IAcUserBase : IEntityGuid, IAcProfileForeignKey, IEmailAddress, IEmailConfirmed, IPassword, ITimeStampInfo { - public string PhoneNumber { get; set; } + public string? PhoneNumber { get; set; } public string? RefreshToken { get; set; } public Guid? RefferalId { get; set; } diff --git a/AyCode.Models.Server/Logins/AcLoggedInModelServer.cs b/AyCode.Models.Server/Logins/AcLoggedInModelServer.cs new file mode 100644 index 0000000..cab634a --- /dev/null +++ b/AyCode.Models.Server/Logins/AcLoggedInModelServer.cs @@ -0,0 +1,17 @@ +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.Users; +using AyCode.Models.Logins; + +namespace AyCode.Models.Server.Logins; + +public class AcLoggedInModelServer : IAcLoggedInModelBase + where TUser : class, IAcUser + where TProfile : class, IAcProfile + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + where TProfileAddress : class, IAcAddress +{ + public TUser? LoggedInUser { get; set; } +} \ No newline at end of file diff --git a/AyCode.Models/AyCode.Models.csproj b/AyCode.Models/AyCode.Models.csproj index 4362a07..086bd2b 100644 --- a/AyCode.Models/AyCode.Models.csproj +++ b/AyCode.Models/AyCode.Models.csproj @@ -6,10 +6,6 @@ enable - - - - diff --git a/AyCode.Models/Logins/IAcLoggedInModelBase.cs b/AyCode.Models/Logins/IAcLoggedInModelBase.cs new file mode 100644 index 0000000..2dd9bea --- /dev/null +++ b/AyCode.Models/Logins/IAcLoggedInModelBase.cs @@ -0,0 +1,18 @@ +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Logins; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.TimeStampInfo; +using AyCode.Interfaces.Users; + +namespace AyCode.Models.Logins; + +public interface IAcLoggedInModelBase : IAcLoginDtoBase + where TUser : class, IAcUser + where TProfile : class, IAcProfile + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + where TProfileAddress : class, IAcAddress +{ + TUser? LoggedInUser { get; set; } +} \ No newline at end of file diff --git a/AyCode.Models/Logins/IAcLoginDtoBase.cs b/AyCode.Models/Logins/IAcLoginDtoBase.cs index 805ae70..d1d901b 100644 --- a/AyCode.Models/Logins/IAcLoginDtoBase.cs +++ b/AyCode.Models/Logins/IAcLoginDtoBase.cs @@ -2,7 +2,7 @@ namespace AyCode.Models.Logins; -public interface IAcLoginDtoBase : IEntityGuid +public interface IAcLoginDtoBase { } \ No newline at end of file diff --git a/AyCode.Services.Server.Tests/AyCode.Services.Server.Tests.csproj b/AyCode.Services.Server.Tests/AyCode.Services.Server.Tests.csproj new file mode 100644 index 0000000..482e437 --- /dev/null +++ b/AyCode.Services.Server.Tests/AyCode.Services.Server.Tests.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AyCode.Services.Server.Tests/LoginServices/AcLoginServiceServerTestBase.cs b/AyCode.Services.Server.Tests/LoginServices/AcLoginServiceServerTestBase.cs new file mode 100644 index 0000000..1e63548 --- /dev/null +++ b/AyCode.Services.Server.Tests/LoginServices/AcLoginServiceServerTestBase.cs @@ -0,0 +1,30 @@ +using AyCode.Database.DataLayers.Users; +using AyCode.Database.DbContexts.Users; +using AyCode.Database.DbContexts; +using AyCode.Database.Tests; +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.Server.Logins; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.Users; + +namespace AyCode.Services.Server.Tests.LoginServices +{ + public abstract class AcLoginServiceServerTestBase : AcDatabaseTestModelBase + where TDal : AcUserDalBase + where TDbContext : AcDbContextBase, IAcUserDbContextBase + where TLoginServiceServer : class, IAcLoginServiceServer + where TUser : class, IAcUser + where TProfile : class, IAcProfile + where TProfileAddress : class, IAcAddress + where TUserToken : class, IAcUserTokenBase + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + { + public TUser RegisterUserTest() + { + //Activator.CreateInstance(typeof(TLoginServiceServer), ) + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/AyCode.Services.Server/AyCode.Services.Server.csproj b/AyCode.Services.Server/AyCode.Services.Server.csproj index 945146c..20fdac1 100644 --- a/AyCode.Services.Server/AyCode.Services.Server.csproj +++ b/AyCode.Services.Server/AyCode.Services.Server.csproj @@ -8,7 +8,7 @@ - + diff --git a/AyCode.Services.Server/Logins/AcLoginServiceServer.cs b/AyCode.Services.Server/Logins/AcLoginServiceServer.cs new file mode 100644 index 0000000..16d8c8a --- /dev/null +++ b/AyCode.Services.Server/Logins/AcLoginServiceServer.cs @@ -0,0 +1,179 @@ +using System.IdentityModel.Tokens.Jwt; +using AyCode.Core.Helpers; +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Logins; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.Server.Logins; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.Users; +using AyCode.Services.Logins; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using AyCode.Database.DataLayers; +using Microsoft.EntityFrameworkCore; +using AyCode.Database.DbContexts; +using AyCode.Core.Consts; +using AyCode.Core.Extensions; +using AyCode.Database.DataLayers.Users; +using AyCode.Database.DbContexts.Users; +using AyCode.Models.Logins; +using AyCode.Models.Server.Logins; +using AyCode.Utils.Extensions; +using AyCode.Utils.Helpers; +using Microsoft.Extensions.Configuration; + +namespace AyCode.Services.Server.Logins; + +public class AcLoginServiceServer(TDal userDal, IConfiguration configuration) + : AcLoginServiceBase, IAcLoginServiceServer + + where TResultLoggedInModel: class, IAcLoggedInModelBase + where TDal : AcUserDalBase + where TDbContext : AcDbContextBase, IAcUserDbContextBase + where TUser : class, IAcUser + where TUserToken : class, IAcUserTokenBase + where TProfile : class, IAcProfile + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + where TProfileAddress : class, IAcAddress +{ + public virtual TUser? Login(string email, string password, out string accessToken) + { + accessToken = string.Empty; + //var loginModel = Activator.CreateInstance(); + + if (!AcValidate.IsValidEmailAndPasswordFormat(email, password, out var errorCode)) return null;//return loginModel; + + var user = userDal.GetUserByEmail(email); + if (user is not { EmailConfirmed: true } || !string.Equals(user.EmailAddress, email, StringComparison.CurrentCultureIgnoreCase) + || !PasswordHasher.VerifyPassword(password, user.Password, GenerateDynamicSalt(user.Id))) return null; + + + //loginModel.LoggedInUser = user; + return LoggedInUser = user; + } + + public virtual Task LoginAsync(string email, string password) + { + return TaskHelper.ToThreadPoolTask(() => Login(email, password, out _)); + } + + public virtual bool Logout() + { + LoggedInUser = null; + + return true; + } + + public virtual Task LogoutAsync() + { + return TaskHelper.ToThreadPoolTask(Logout); + } + + public virtual TUser? Registration(string email, string password, string? phoneNumber = null) + => Registration(Guid.NewGuid(), email, password, phoneNumber); + + public virtual TUser? Registration(Guid userId, string email, string password, string? phoneNumber = null) + { + if ((phoneNumber != null && !AcValidate.IsValidPhoneNumberFormat(phoneNumber, out var errorCode)) || + !AcValidate.IsValidEmailAndPasswordFormat(email, password, out errorCode)) return null; + + var user = Activator.CreateInstance(); + user.Id = userId; + user.EmailAddress = email; + user.EmailConfirmed = true; + user.Password = PasswordHasher.HashPassword(password, GenerateDynamicSalt(userId)); + + var address = Activator.CreateInstance(); + address.Id = Guid.NewGuid(); + + var userName = email.Split('@')[0]; //TODO: generálni egy nevet... - J. + return userDal.AddUser(user, userName, address) ? null : userDal.GetUserById(userId); + } + + public virtual Task RegistrationAsync(string email, string password, string? phoneNumber = null) + => RegistrationAsync(Guid.NewGuid(), email, password, phoneNumber); + public virtual Task RegistrationAsync(Guid userId, string email, string password, string? phoneNumber = null) + { + return TaskHelper.ToThreadPoolTask(() => Registration(userId, email, password, phoneNumber)); + } + + //SEMMILYEN KÖRÜLMÉNYEK KÖZÖTT SE VÁLTOZTASD MEG METÓDUS LOGIKÁJÁT!!! Különben senki sem fog tudni Login-olni! - J. + public virtual string GenerateDynamicSalt(Guid userId) + => userId.ToString("N").ToLower().Reverse().MixCharacters(AcConst.ProjectSalt); + + private string GenerateAccessToken(TUser user) + { + var tokenHandler = new JwtSecurityTokenHandler(); + Console.WriteLine("----------------------------------------------------------"); + + if (configuration["JWT:Key"] == null) + throw new SecurityTokenException("Token is null"); + + var keyDetail = Encoding.UTF8.GetBytes(configuration["JWT:Key"] ?? string.Empty); + Console.WriteLine(configuration["JWT:Key"]); + + var claims = new List + { + new(ClaimTypes.NameIdentifier, user.Id.ToString()), + new(ClaimTypes.Email, user.EmailAddress) + }; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Audience = configuration["JWT:Audience"], + Issuer = configuration["JWT:Issuer"], + Expires = DateTime.UtcNow.AddMinutes(30), + Subject = new ClaimsIdentity(claims), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(keyDetail), SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor) as JwtSecurityToken; + var writtenToken = tokenHandler.WriteToken(token); + Console.WriteLine(writtenToken); + + return writtenToken; + } + + private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + if (configuration["JWT:Key"] == null) + throw new SecurityTokenException("Token is null"); + + var keyDetail = Encoding.UTF8.GetBytes(configuration["JWT:Key"] ?? string.Empty); + + var tokenValidationParameter = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + ValidateIssuerSigningKey = true, + ValidIssuer = configuration["JWT:Issuer"], + ValidAudience = configuration["JWT:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(keyDetail), + }; + + var principal = tokenHandler.ValidateToken(token, tokenValidationParameter, out var securityToken); + + if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) + throw new SecurityTokenException("Invalid token"); + + return principal; + } + + private string GenerateRefreshToken() + { + var randomNumber = new byte[32]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + } +} \ No newline at end of file diff --git a/AyCode.Services/AyCode.Services.csproj b/AyCode.Services/AyCode.Services.csproj index c68d0c6..96f7971 100644 --- a/AyCode.Services/AyCode.Services.csproj +++ b/AyCode.Services/AyCode.Services.csproj @@ -6,6 +6,10 @@ enable + + + + diff --git a/AyCode.Services/Logins/AcLoginServiceBase.cs b/AyCode.Services/Logins/AcLoginServiceBase.cs new file mode 100644 index 0000000..e9e1ce9 --- /dev/null +++ b/AyCode.Services/Logins/AcLoginServiceBase.cs @@ -0,0 +1,17 @@ +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Logins; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.Users; + +namespace AyCode.Services.Logins; + +public abstract class AcLoginServiceBase : IAcLoginServiceBase + where TUser : class, IAcUser + where TProfile : class, IAcProfile + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + where TProfileAddress : class, IAcAddress +{ + public TUser? LoggedInUser { get; protected set; } +} \ No newline at end of file diff --git a/AyCode.Services/Logins/AcLoginServiceClient.cs b/AyCode.Services/Logins/AcLoginServiceClient.cs new file mode 100644 index 0000000..4de231a --- /dev/null +++ b/AyCode.Services/Logins/AcLoginServiceClient.cs @@ -0,0 +1,58 @@ +using AyCode.Core.Helpers; +using AyCode.Interfaces.Addresses; +using AyCode.Interfaces.Logins; +using AyCode.Interfaces.Profiles; +using AyCode.Interfaces.ServiceProviders; +using AyCode.Interfaces.Users; + +namespace AyCode.Services.Logins; + +public class AcLoginServiceClient + : AcLoginServiceBase, IAcLoginServiceClient + + where TUser : class, IAcUser + where TProfile : class, IAcProfile + where TServiceProvider : class, IAcServiceProviderBase + where TUserToServiceProvider : class, IAcUserToServiceProviderBase + where TProfileAddress : class, IAcAddress +{ + public virtual TUser? Login(string email, string password, out string accessToken) + { + throw new NotImplementedException(); + } + + public virtual Task LoginAsync(string email, string password) + { + return TaskHelper.ToThreadPoolTask(() => Login(email, password, out _)); + } + + public virtual bool Logout() + { + throw new NotImplementedException(); + } + + public virtual Task LogoutAsync() + { + return TaskHelper.ToThreadPoolTask(Logout); + } + + public virtual TUser? Registration(string email, string password, string? phoneNumber = null) + { + throw new NotImplementedException(); + } + + public TUser? Registration(Guid userId, string email, string password, string? phoneNumber = null) + { + throw new NotImplementedException(); + } + + public virtual Task RegistrationAsync(string email, string password, string? phoneNumber = null) + { + return TaskHelper.ToThreadPoolTask(() => Registration(email, password, phoneNumber)); + } + + public Task RegistrationAsync(Guid userId, string email, string password, string? phoneNumber = null) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/AyCode.Utils/AyCode.Utils.csproj b/AyCode.Utils/AyCode.Utils.csproj index eb7c41d..115ed45 100644 --- a/AyCode.Utils/AyCode.Utils.csproj +++ b/AyCode.Utils/AyCode.Utils.csproj @@ -8,7 +8,7 @@ - + diff --git a/AyCode.Utils/Extensions/StringExtensions.cs b/AyCode.Utils/Extensions/StringExtensions.cs index 48c0174..88bd5ae 100644 --- a/AyCode.Utils/Extensions/StringExtensions.cs +++ b/AyCode.Utils/Extensions/StringExtensions.cs @@ -1,7 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using System.Text; using JetBrains.Annotations; -namespace AyCode.Utils.Extensions +namespace AyCode.Core.Extensions { public static class StringExtensions { @@ -36,5 +37,41 @@ namespace AyCode.Utils.Extensions return char.ToUpper(str[0]) + str[1..]; } + + + //public static string MixCharacters(this string str1, string str2) + // => MixCharacters(str1?.ToCharArray(), str2?.ToCharArray()); + + //public static string MixCharacters(this char[] chars1, string str2) + // => MixCharacters(chars1, str2?.ToCharArray()); + + //public static string MixCharacters(this string str1, char[] chars2) + // => MixCharacters(str1?.ToCharArray(), chars2); + + /// + /// Example : chars1=ABC, chars2=DEF ==> ADBECF + /// + /// + /// + /// + public static string MixCharacters(this IEnumerable firstChars, IEnumerable secondChars) + { + var chars1 = firstChars as char[] ?? firstChars.ToArray(); + var chars2 = secondChars as char[] ?? secondChars.ToArray(); + + if (chars1 is not { Length: > 0 }) return chars2.ToString() ?? string.Empty; + if (chars2 is not { Length: > 0 }) return chars1.ToString() ?? string.Empty; + + var length = chars1.Length < chars2.Length ? chars1.Length : chars2.Length; + var builder = new StringBuilder(length * 2); + + for (var i = 0; i < length; i++) + { + builder.Append(chars1[i]); + builder.Append(chars2[i]); + } + + return builder.ToString(); + } } } diff --git a/AyCode.Utils/Helpers/PasswordHasher.cs b/AyCode.Utils/Helpers/PasswordHasher.cs index 44433f3..71a5924 100644 --- a/AyCode.Utils/Helpers/PasswordHasher.cs +++ b/AyCode.Utils/Helpers/PasswordHasher.cs @@ -1,51 +1,50 @@ using Microsoft.AspNetCore.Cryptography.KeyDerivation; using System.Security.Cryptography; +using System.Text; +using AyCode.Core.Extensions; +using AyCode.Utils.Extensions; namespace AyCode.Utils.Helpers { public class PasswordHasher { - public string HashPassword(string password) + public static string HashPassword(string password, string? dynamicSalt = null) { // Generate a random salt - byte[] salt = new byte[16]; - using (var rng = RandomNumberGenerator.Create()) - { - rng.GetBytes(salt); - } + var salt = new byte[16]; + using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(salt); // Hash the password with the salt - string hashedPassword = Convert.ToBase64String(KeyDerivation.Pbkdf2( - password: password, - salt: salt, - prf: KeyDerivationPrf.HMACSHA512, - iterationCount: 10000, - numBytesRequested: 32)); + var hashedPassword = GenerateHashedPassword(password, salt, dynamicSalt); // Combine the salt and hashed password - string combinedHash = $"$bcrypt$v=1$salt={Convert.ToBase64String(salt)}$hash={hashedPassword}"; + var combinedHash = $"$bcrypt$v=1$salt={Convert.ToBase64String(salt)}$hash={hashedPassword}"; return combinedHash; } - public bool VerifyPassword(string password, string hashedPassword) + public static bool VerifyPassword(string password, string hashedPassword, string? dynamicSalt = null) { // Extract the salt and hashed password from the combined hash - string[] parts = hashedPassword.Split('$'); - byte[] salt = Convert.FromBase64String(parts[3]); - string storedHash = parts[5]; + var parts = hashedPassword.Split('$'); + var salt = Convert.FromBase64String(parts[3].Replace("salt=", string.Empty)); + var storedHash = parts[4].Replace("hash=", string.Empty); - // Hash the provided password with the extracted salt - string hashedProvidedPassword = Convert.ToBase64String(KeyDerivation.Pbkdf2( + return storedHash == GenerateHashedPassword(password, salt, dynamicSalt); + } + + private static string GenerateHashedPassword(string password, byte[] salt, string? dynamicSalt) + => Convert.ToBase64String(KeyDerivation.Pbkdf2( password: password, - salt: salt, + salt: GenerateFinallySalt(salt, dynamicSalt), prf: KeyDerivationPrf.HMACSHA512, iterationCount: 10000, numBytesRequested: 32)); - // Compare the hashed passwords - return storedHash == hashedProvidedPassword; - } + //SEMMILYEN KÖRÜLMÉNYEK KÖZÖTT SE VÁLTOZTASD MEG METÓDUS LOGIKÁJÁT!!! Különben senki sem fog tudni Login-olni! - J. + private static byte[] GenerateFinallySalt(byte[] salt, string? dynamicSalt) + => SHA256.HashData(string.IsNullOrWhiteSpace(dynamicSalt) ? salt : Encoding.ASCII.GetBytes(Convert.ToBase64String(salt).Reverse().MixCharacters(dynamicSalt))).Take(16).ToArray(); + } }