diff --git a/Surge365.MassEmailReact.API/Program.cs b/Surge365.MassEmailReact.API/Program.cs index 6371212..00f6836 100644 --- a/Surge365.MassEmailReact.API/Program.cs +++ b/Surge365.MassEmailReact.API/Program.cs @@ -1,6 +1,8 @@ +using Azure.Identity; using Microsoft.AspNetCore.Identity; using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; +using Surge365.MassEmailReact.Infrastructure; using Surge365.MassEmailReact.Infrastructure.DapperMaps; using Surge365.MassEmailReact.Infrastructure.Repositories; using Surge365.MassEmailReact.Infrastructure.Services; @@ -10,6 +12,18 @@ using System.Security.Authentication; var builder = WebApplication.CreateBuilder(args); + +var keyVaultName = builder.Configuration["KeyVaultName"] ?? ""; +if (!string.IsNullOrEmpty(keyVaultName)) +{ + var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/"; + builder.Configuration.AddAzureKeyVault( + new Uri(keyVaultUri), + new DefaultAzureCredential(), + new SurgeKeyVaultSecretManager() + ); +} + builder.Services.AddHttpClient("SendGridClient", client => { client.BaseAddress = new Uri("https://api.sendgrid.com/"); // Optional, for clarity @@ -22,7 +36,6 @@ builder.Services.AddHttpClient("SendGridClient", client => builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Surge365.MassEmailReact.API/appsettings.Development.json b/Surge365.MassEmailReact.API/appsettings.Development.json index 128b0e3..4525358 100644 --- a/Surge365.MassEmailReact.API/appsettings.Development.json +++ b/Surge365.MassEmailReact.API/appsettings.Development.json @@ -1,4 +1,5 @@ { + "KeyVaultName": "surge365-keyvault-dev", "Logging": { "LogLevel": { "Default": "Information", @@ -6,13 +7,11 @@ } }, "AllowedHosts": "*", - "Jwt": { - "Secret": "Z9R5aFml+eRMeb7tyf8N9wCq3tZpS/EM6nGqOxlXPtOw4cJ3zS1AByczrIlD5F9d" - }, "EnvironmentCode": "UAT", "ConnectionStrings": { - "Marketing.ConnectionString": "data source=uat.surge365.com;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;", //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT - "MassEmail.ConnectionString": "data source=uat.surge365.com;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT + }, + "AdminAuth": { + "Url": "https://uat.aauth.surge365.com" }, "DefaultUnsubscribeUrl": "https://uat.emailopentracking.surge365.com/unsubscribe.htm" } \ No newline at end of file diff --git a/Surge365.MassEmailReact.API/appsettings.Uat.json b/Surge365.MassEmailReact.API/appsettings.Uat.json index 5c857ec..75115c5 100644 --- a/Surge365.MassEmailReact.API/appsettings.Uat.json +++ b/Surge365.MassEmailReact.API/appsettings.Uat.json @@ -1,4 +1,5 @@ { + "KeyVaultName": "surge365-keyvault-uat", "Logging": { "LogLevel": { "Default": "Information", @@ -6,13 +7,9 @@ } }, "AllowedHosts": "*", - "Jwt": { - "Secret": "1bXgXk7v/W9XksGoNiqWvM7+9/BERZonShxqoCVvdi8Ew47M1VFzJGA9sPMgkmn/HRmuZ83iytNsHXI6GkAb8g==" - }, "EnvironmentCode": "UAT", - "ConnectionStrings": { - "Marketing.ConnectionString": "data source=localhost;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;", //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT - "MassEmail.ConnectionString": "data source=localhost;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT + "AdminAuth": { + "Url": "https://uat.aauth.surge365.com" }, "DefaultUnsubscribeUrl": "https://uat.emailopentracking.surge365.com/unsubscribe.htm" } \ No newline at end of file diff --git a/Surge365.MassEmailReact.API/appsettings.json b/Surge365.MassEmailReact.API/appsettings.json index 9d04617..cc744ab 100644 --- a/Surge365.MassEmailReact.API/appsettings.json +++ b/Surge365.MassEmailReact.API/appsettings.json @@ -1,4 +1,5 @@ { + "KeyVaultName": "surge365-keyvault-prod", "Logging": { "LogLevel": { "Default": "Information", @@ -6,15 +7,11 @@ } }, "AllowedHosts": "*", - "Jwt": { - "Secret": "4r1AJ0riBpEhgaTxhTWMIPs5rv9AlVZjTqrGUoU3DUz4i/Dx9ZfGciIubNODQRO0z3qJZq6VqxGXdsFRJgSb6Q==" - }, "AppCode": "MassEmailReactApi", "AuthAppCode": "MassEmailWeb", "EnvironmentCode": "PRODUCTION", - "ConnectionStrings": { - "Marketing.ConnectionString": "data source=localhost;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;", //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT - "MassEmail.ConnectionString": "data source=localhost;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT + "AdminAuth": { + "Url": "https://aauth.surge365.com" }, "TestTargetSql": "CREATE TABLE #columns\r\n(\r\n primary_key INT NOT NULL IDENTITY(1,1) PRIMARY KEY,\r\n name VARCHAR(255),\r\n data_type CHAR(1)\r\n)\r\nSELECT TOP 10 *\r\nINTO #list\r\nFROM ##database_name##..##view_name##\r\n##filter##\r\n\r\nDECLARE @row_count INT\r\nSELECT @row_count = COUNT(*)\r\nFROM ##database_name##..##view_name##\r\n##filter##\r\n\r\nDECLARE c_curs CURSOR FOR \r\nSELECT c.name AS column_name, t.name AS data_type\r\nFROM tempdb.sys.columns c\r\nINNER JOIN tempdb.sys.types t ON c.user_type_id = t.user_type_id\r\n AND t.name NOT IN ('text','ntext','image','binary','varbinary','image','cursor','timestamp','hierarchyid','sql_variant','xml','table')\r\nWHERE object_id = object_id('tempdb..#list') \r\n AND ((t.name IN ('char','varchar') AND c.max_length <= 255)\r\n OR (t.name IN ('nchar','nvarchar') AND c.max_length <= 510)\r\n OR (t.name NOT IN ('char','varchar','nchar','nvarchar')))\r\n \r\nOPEN c_curs\r\nDECLARE @column_name VARCHAR(255), @column_type VARCHAR(255)\r\n\r\nFETCH NEXT FROM c_curs INTO @column_name, @column_type\r\nWHILE(@@FETCH_STATUS = 0)\r\nBEGIN \r\n DECLARE @data_type CHAR(1) = 'S'\r\n IF(@column_type IN ('date','datetime','datetime2','datetimeoffset','smalldatetime','time'))\r\n BEGIN\r\n SET @data_type = 'D'\r\n END\r\n ELSE IF(@column_type IN ('bit'))\r\n BEGIN\r\n SET @data_type = 'B'\r\n END\r\n ELSE IF(@column_type IN ('bigint','numeric','smallint','decimal','smallmoney','int','tinyint','money','float','real'))\r\n BEGIN\r\n SET @data_type = 'N'\r\n END\r\n INSERT INTO #columns(name, data_type) VALUES(@column_name, @data_type)\r\n FETCH NEXT FROM c_curs INTO @column_name, @column_type\r\nEND\r\nCLOSE c_curs\r\nDEALLOCATE c_curs\r\nSELECT * FROM #columns ORDER BY primary_key\r\nSELECT * FROM #list\r\nSELECT @row_count AS row_count\r\nDROP TABLE #columns\r\nDROP TABLE #list", "ConnectionStringTemplate": "data source=##server_name##,##port##;initial catalog=##database_name##;User ID=##username##;Password=##password##;persist security info=False;packet size=4096;TrustServerCertificate=True;", diff --git a/Surge365.MassEmailReact.Application/DTOs/AuthApi/AuthResponse.cs b/Surge365.MassEmailReact.Application/DTOs/AuthApi/AuthResponse.cs new file mode 100644 index 0000000..7958a12 --- /dev/null +++ b/Surge365.MassEmailReact.Application/DTOs/AuthApi/AuthResponse.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Application.DTOs.AuthApi +{ + public class AuthResponse + { + public string accessToken { get; set; } = ""; + public string refreshToken { get; set; } = ""; + public User? user { get; set; } + public string message { get; set; } = ""; + } +} diff --git a/Surge365.MassEmailReact.Application/DTOs/AuthApi/AuthenticateApiRequest.cs b/Surge365.MassEmailReact.Application/DTOs/AuthApi/AuthenticateApiRequest.cs new file mode 100644 index 0000000..2b457ac --- /dev/null +++ b/Surge365.MassEmailReact.Application/DTOs/AuthApi/AuthenticateApiRequest.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Application.DTOs.AuthApi +{ + public class AuthenticateApiRequest + { + public required string appCode { get; set; } + public required string username { get; set; } + public required string password { get; set; } + } +} diff --git a/Surge365.MassEmailReact.Application/DTOs/AuthApi/RefreshTokenApiRequest.cs b/Surge365.MassEmailReact.Application/DTOs/AuthApi/RefreshTokenApiRequest.cs new file mode 100644 index 0000000..5f21cdd --- /dev/null +++ b/Surge365.MassEmailReact.Application/DTOs/AuthApi/RefreshTokenApiRequest.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Application.DTOs.AuthApi +{ + public class RefreshTokenApiRequest + { + public string appCode { get; set; } = ""; + public string refreshToken { get; set; } = ""; + } +} diff --git a/Surge365.MassEmailReact.Application/DTOs/AuthApi/User.cs b/Surge365.MassEmailReact.Application/DTOs/AuthApi/User.cs new file mode 100644 index 0000000..12dcf57 --- /dev/null +++ b/Surge365.MassEmailReact.Application/DTOs/AuthApi/User.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Application.DTOs.AuthApi +{ + public class User + { + public int? UserKey { get; set; } + public Guid UserId { get; set; } + public string Username { get; set; } = ""; + public string FirstName { get; set; } = ""; + public string MiddleInitial { get; set; } = ""; + public string LastName { get; set; } = ""; + public bool IsActive { get; set; } + public List Roles { get; set; } = new List(); + + public User() { } + } +} diff --git a/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs b/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs index 3cd9c65..1339cb2 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs @@ -1,4 +1,4 @@ -using Surge365.MassEmailReact.Domain.Entities; +using Surge365.MassEmailReact.Application.DTOs.AuthApi; namespace Surge365.MassEmailReact.Application.Interfaces { diff --git a/Surge365.MassEmailReact.Application/Interfaces/IUserRepository.cs b/Surge365.MassEmailReact.Application/Interfaces/IUserRepository.cs deleted file mode 100644 index 29e52b8..0000000 --- a/Surge365.MassEmailReact.Application/Interfaces/IUserRepository.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Surge365.MassEmailReact.Domain.Entities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Surge365.MassEmailReact.Application.Interfaces -{ - public interface IUserRepository - { - Task<(User? user, string message)> Authenticate(string username, string password); - Task Authenticate(string refreshToken); - Task GetByUsername(string username); - Task GetByKey(int userKey); - Task GetById(Guid userId); - Task> GetAll(bool activeOnly = true); - - Task SaveRefreshToken(Guid userId, string refreshToken, string? previousToken = ""); - } -} diff --git a/Surge365.MassEmailReact.Domain/Entities/User.cs b/Surge365.MassEmailReact.Domain/Entities/User.cs deleted file mode 100644 index b2f7f9c..0000000 --- a/Surge365.MassEmailReact.Domain/Entities/User.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Surge365.MassEmailReact.Domain.Entities -{ - public class User - { - public int? UserKey { get; private set; } - public Guid UserId { get; private set; } - public string Username { get; private set; } = ""; - public string FirstName { get; private set; } = ""; - public string MiddleInitial { get; private set; } = ""; - public string LastName { get; private set; } = ""; - public bool IsActive { get; private set; } - public List Roles { get; private set; } = new List(); - - public User() { } - //private User(int userKey, Guid userId, string username, string? firstName, string? middleInitial, string? lastName, bool isActive, List roles) - //{ - // UserKey = userKey; - // UserId = userId; - // Username = username; - // FirstName = firstName ?? ""; - // MiddleInitial = middleInitial ?? ""; - // LastName = lastName ?? ""; - // IsActive = isActive; - // Roles = roles; - //} - //public static User Create(int userKey, Guid userId, string username, string? firstName, string? middleInitial, string? lastName, bool isActive, List roles) - //{ - // return new User(userKey, userId, username, firstName, middleInitial, lastName, isActive, roles); - //} - } -} diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs index 0eaa19b..69ac3e6 100644 --- a/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs +++ b/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs @@ -30,7 +30,6 @@ namespace Surge365.MassEmailReact.Infrastructure.DapperMaps config.AddMap(new MailingTemplateMap()); config.AddMap(new MailingTargetMap()); config.AddMap(new MailingStatisticMap()); - config.AddMap(new UserMap()); }); } } diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/UserMap.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/UserMap.cs deleted file mode 100644 index 90f34a3..0000000 --- a/Surge365.MassEmailReact.Infrastructure/DapperMaps/UserMap.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Dapper.FluentMap.Mapping; -using Surge365.MassEmailReact.Domain.Entities; - -namespace Surge365.MassEmailReact.Infrastructure.DapperMaps -{ - public class UserMap : EntityMap - { - public UserMap() - { - Map(u => u.UserKey).ToColumn("login_key"); - Map(u => u.UserId).ToColumn("login_id"); - Map(u => u.Username).ToColumn("username"); - Map(u => u.FirstName).ToColumn("first_name"); - Map(u => u.MiddleInitial).ToColumn("middle_initial"); - Map(u => u.LastName).ToColumn("last_name"); - Map(u => u.IsActive).ToColumn("is_active"); - Map(u => u.Roles).ToColumn("roles"); - } - } -} diff --git a/Surge365.MassEmailReact.Infrastructure/KeyVaultSecretManager.cs b/Surge365.MassEmailReact.Infrastructure/KeyVaultSecretManager.cs new file mode 100644 index 0000000..03e051d --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/KeyVaultSecretManager.cs @@ -0,0 +1,36 @@ +using Azure.Extensions.AspNetCore.Configuration.Secrets; +using Azure.Security.KeyVault.Secrets; + +namespace Surge365.MassEmailReact.Infrastructure +{ + public class SurgeKeyVaultSecretManager : KeyVaultSecretManager + { + public override bool Load(SecretProperties secret) + { + return true; + } + public override string GetKey(KeyVaultSecret secret) + { + // Transform secret name back to configuration key + // Replace double hyphens (--) with colons (:) and single hyphens (-) with periods (.) + /* + * Example: + * AppSettings.json = + { + "AdminAuth": { + "ApiKey": "abcde" + }, + "AdminAuth.ConnectionString":"abc" + } + * _config path = "AdminAuth:ApiKey" + * Environment Variable = "AdminAuth__ApiKey" + * Azure Key Vault = "AdminAuth--ApiKey" + * + * _config path = "AdminAuth.ConnectionString" + * Environment Variable = "AdminAuth.ConnectionString" + * Azure Key Vault = "AdminAuth-ConnectionString" + */ + return secret.Name.Replace("--", ":").Replace("-", "."); + } + } +} diff --git a/Surge365.MassEmailReact.Infrastructure/Repositories/UserRepository.cs b/Surge365.MassEmailReact.Infrastructure/Repositories/UserRepository.cs deleted file mode 100644 index bc5b3b3..0000000 --- a/Surge365.MassEmailReact.Infrastructure/Repositories/UserRepository.cs +++ /dev/null @@ -1,167 +0,0 @@ -using Dapper; -using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Configuration; -using Surge365.MassEmailReact.Application.Interfaces; -using Surge365.MassEmailReact.Domain.Entities; -using Surge365.MassEmailReact.Domain.Enums; -using Surge365.MassEmailReact.Domain.Enums.Extensions; -using System; -using System.Collections.Generic; -using System.Data; -using System.Threading.Tasks; - -namespace Surge365.MassEmailReact.Infrastructure.Repositories -{ - public class UserRepository : IUserRepository - { - private readonly IConfiguration _config; - private const string _connectionStringName = "Marketing.ConnectionString"; - private const string _refreshTokenConnectionStringName = "MassEmail.ConnectionString"; - - public UserRepository(IConfiguration config) - { - _config = config ?? throw new ArgumentNullException(nameof(config)); - } - - public string AppCode - { - get - { - return _config?["AuthAppCode"] ?? Utilities.GetAppCode(_config); - } - } - - private string ConnectionString => _config.GetConnectionString(_connectionStringName) ?? ""; - private string RefreshTokenConnectionString => _config.GetConnectionString(_refreshTokenConnectionStringName) ?? ""; - - public async Task<(User? user, string message)> Authenticate(string username, string password) - { - using var connection = new SqlConnection(ConnectionString); - var parameters = new DynamicParameters(); - parameters.Add("username", username); - parameters.Add("password", password); - parameters.Add("application_code", AppCode); - parameters.Add("response_number", dbType: DbType.Int16, direction: ParameterDirection.Output); - parameters.Add("login_key", dbType: DbType.Int32, direction: ParameterDirection.Output); - - var result = await connection.QueryAsync( - "adm_authenticate_login", - parameters, - commandType: CommandType.StoredProcedure - ); - - var responseNumber = parameters.Get("response_number"); - var authResult = (AuthResult)responseNumber; - string responseMessage = authResult.GetMessage(); - - if (authResult == AuthResult.Success) - { - var user = result.FirstOrDefault(); - return (user, responseMessage); - } - return (null, responseMessage); - } - - public async Task Authenticate(string refreshToken) - { - using var connection = new SqlConnection(RefreshTokenConnectionString); - var parameters = new DynamicParameters(); - parameters.Add("@@token_hash", HashToken(refreshToken)); - parameters.Add("@valid", dbType: DbType.Boolean, direction: ParameterDirection.Output); - parameters.Add("@user_guid", dbType: DbType.Guid, direction: ParameterDirection.Output); - - await connection.ExecuteAsync( - "mem_validate_user_refresh_token", - parameters, - commandType: CommandType.StoredProcedure - ); - - if (!parameters.Get("@valid")) - return null; - - return await GetById(parameters.Get("@user_guid")); - } - - public async Task GetByUsername(string username) - { - using var connection = new SqlConnection(ConnectionString); - var parameters = new { username, application_code = AppCode }; - return await connection.QueryFirstOrDefaultAsync( - "adm_get_login_by_username", - parameters, - commandType: CommandType.StoredProcedure - ); - } - - public async Task GetByKey(int userKey) - { - using var connection = new SqlConnection(ConnectionString); - var parameters = new { login_key = userKey, application_code = AppCode }; - return await connection.QueryFirstOrDefaultAsync( - "adm_get_adm_login_by_key", - parameters, - commandType: CommandType.StoredProcedure - ); - } - - public async Task GetById(Guid userId) - { - using var connection = new SqlConnection(ConnectionString); - var parameters = new { login_id = userId, application_code = AppCode }; - return await connection.QueryFirstOrDefaultAsync( - "adm_get_adm_login_by_id", - parameters, - commandType: CommandType.StoredProcedure - ); - } - - public async Task> GetAll(bool activeOnly = true) - { - using var connection = new SqlConnection(ConnectionString); - var parameters = new { active_only = activeOnly, application_code = AppCode }; - var users = await connection.QueryAsync( - "adm_get_adm_login_all", - parameters, - commandType: CommandType.StoredProcedure - ); - return users.AsList(); - } - public async Task SaveRefreshToken(Guid userId, string token, string? previousToken = "") - { - using var connection = new SqlConnection(RefreshTokenConnectionString); - var parameters = new DynamicParameters(); - parameters.Add("@user_guid", userId); - parameters.Add("@token_hash", HashToken(token)); - parameters.Add("@previous_token_hash", HashToken(previousToken ?? "")); - parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); - - await connection.ExecuteAsync( - "mem_save_user_refresh_token", - parameters, - commandType: CommandType.StoredProcedure - ); - - return parameters.Get("@success"); - } - - private string HashToken(string token) - { - if (string.IsNullOrEmpty(token)) - { - return ""; - } - - using (var sha256 = System.Security.Cryptography.SHA256.Create()) - { - // Convert the token string to bytes - byte[] tokenBytes = System.Text.Encoding.UTF8.GetBytes(token); - - // Compute the hash - byte[] hashBytes = sha256.ComputeHash(tokenBytes); - - // Convert the hash to a hexadecimal string - return Convert.ToHexString(hashBytes).ToLowerInvariant(); - } - } - } -} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs b/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs index 7444fba..cd10145 100644 --- a/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs +++ b/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs @@ -8,108 +8,114 @@ using System.Threading.Tasks; using System.IdentityModel.Tokens.Jwt; using Microsoft.IdentityModel.Tokens; using Microsoft.Extensions.Configuration; -using Surge365.MassEmailReact.Domain.Entities; -using System.Security.Cryptography; -using System.Data; +using System.Net.Http.Json; +using Surge365.MassEmailReact.Application.DTOs.AuthApi; namespace Surge365.MassEmailReact.Infrastructure.Services { public class AuthService : IAuthService { - private const int TOKEN_MINUTES = 5; - private readonly IUserRepository _userRepository; private readonly IConfiguration _config; - - public AuthService(IUserRepository userRepository, IConfiguration config) + string ApiUrl + { + get + { + var apiUrl = _config["AdminAuth:Url"]; + if (string.IsNullOrEmpty(apiUrl)) + throw new Exception("Auth URL is not configured"); + return apiUrl; + } + } + string ApiKey + { + get + { + var apiKey = _config["AdminAuth:ApiKey"]; + if (string.IsNullOrEmpty(apiKey)) + throw new Exception("Api Key is not configured"); + return apiKey; + } + } + string AuthAppCode + { + get + { + var appCode = _config["AuthAppCode"]; + if (string.IsNullOrEmpty(appCode)) + throw new Exception("Auth App Code is not configured"); + return appCode; + } + } + + public AuthService(IConfiguration config) { - _userRepository = userRepository; _config = config; } public async Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string refreshToken) { - var user = await _userRepository.Authenticate(refreshToken); - if (user == null) - return (false, null, "Not authenticated"); + var authResponse = await AuthenticateAtApi(refreshToken); + if (!authResponse.success) return (false, null, authResponse.authResponse.message ?? "Authentication failed"); + if (string.IsNullOrWhiteSpace(authResponse.authResponse.accessToken) || string.IsNullOrWhiteSpace(authResponse.authResponse.refreshToken) || authResponse.authResponse.user == null) + return (false, null, authResponse.authResponse.message ?? "Authentication failed"); - var tokenResponse = await GenerateTokens(user.UserId, refreshToken); - if(tokenResponse == null) - return (false, null, "Error generating tokens"); - - return (true, tokenResponse, ""); + return (true, (authResponse.authResponse.user, authResponse.authResponse.accessToken, authResponse.authResponse.refreshToken), ""); } - public async Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string username, string password) { - var authResponse = await _userRepository.Authenticate(username, password); - if (authResponse.user == null) - { - return (false, null, authResponse.message); - } - // Generate JWT token - var tokenHandler = new JwtSecurityTokenHandler(); - var key = Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!); - var claims = new List - { - new Claim(JwtRegisteredClaimNames.Sub, authResponse.user.UserId.ToString()), - new Claim(JwtRegisteredClaimNames.UniqueName, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) - }; - claims.AddRange(authResponse.user.Roles.Select(role => new Claim(ClaimTypes.Role, role))); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(claims), - Expires = DateTime.UtcNow.AddMinutes(TOKEN_MINUTES), - SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha256Signature - ) - }; + var authResponse = await AuthenticateAtApi(username, password); + if (!authResponse.success) return (false, null, "Authentication failed"); - var token = tokenHandler.CreateToken(tokenDescriptor); - var accessToken = tokenHandler.WriteToken(token); - string refreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray()); - if (!await _userRepository.SaveRefreshToken(authResponse.user.UserId, refreshToken)) - return (false, null, "Error saving token"); + if (string.IsNullOrWhiteSpace(authResponse.authResponse.accessToken) || string.IsNullOrWhiteSpace(authResponse.authResponse.refreshToken) || authResponse.authResponse.user == null) + return (false, null, authResponse.authResponse.message ?? "Authentication failed"); - return (true, (authResponse.user, accessToken, refreshToken), ""); + return (true, (authResponse.authResponse.user, authResponse.authResponse.accessToken, authResponse.authResponse.refreshToken), ""); } - private async Task<(User user, string accessToken, string refreshToken)?> GenerateTokens(Guid userId, string previousRefreshToken) + + + async Task<(bool success, AuthResponse authResponse)> AuthenticateAtApi(string refreshToken) { - var user = await _userRepository.GetById(userId); - if (user == null) + var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(ApiUrl); + httpClient.DefaultRequestHeaders.Add("X-Api-Key", ApiKey); + RefreshTokenApiRequest request = new RefreshTokenApiRequest() { - return null; - } - - var tokenHandler = new JwtSecurityTokenHandler(); - var key = Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!); - var claims = new List - { - new Claim(JwtRegisteredClaimNames.Sub, user.UserId.ToString()), - new Claim(JwtRegisteredClaimNames.UniqueName, user.Username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) - }; - claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role))); - - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(claims), - Expires = DateTime.UtcNow.AddMinutes(TOKEN_MINUTES), - SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha256Signature - ) + appCode = AuthAppCode, + refreshToken = refreshToken }; - var token = tokenHandler.CreateToken(tokenDescriptor); - var accessToken = tokenHandler.WriteToken(token); - var newRefreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray()); - if (!await _userRepository.SaveRefreshToken(user.UserId, newRefreshToken, previousRefreshToken)) - return null; + var response = await httpClient.PostAsJsonAsync("authentication/refreshtoken", request); - return (user, accessToken, newRefreshToken); + if (!response.IsSuccessStatusCode) return (false, new AuthResponse() { message = "Authentication failed" }); + + var authResponse = await response.Content.ReadFromJsonAsync(); + + if (authResponse?.user == null) return (false, new AuthResponse() { message = "Authentication failed" }); + + return (true, authResponse); + } + async Task<(bool success, AuthResponse authResponse)> AuthenticateAtApi(string username, string password) + { + var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(ApiUrl); + httpClient.DefaultRequestHeaders.Add("X-Api-Key", ApiKey); + AuthenticateApiRequest request = new AuthenticateApiRequest() + { + appCode = AuthAppCode, + username = username, + password = password + }; + + var response = await httpClient.PostAsJsonAsync("authentication/authenticate", request); + + if (!response.IsSuccessStatusCode) return (false, new AuthResponse() { message = "Authentication failed" }); + + var authResponse = await response.Content.ReadFromJsonAsync(); + + if (authResponse?.user == null) return (false, new AuthResponse() { message = "Authentication failed" }); + + return (true, authResponse); } } } diff --git a/Surge365.MassEmailReact.Infrastructure/Surge365.MassEmailReact.Infrastructure.csproj b/Surge365.MassEmailReact.Infrastructure/Surge365.MassEmailReact.Infrastructure.csproj index 9fec5ce..dbd8ee1 100644 --- a/Surge365.MassEmailReact.Infrastructure/Surge365.MassEmailReact.Infrastructure.csproj +++ b/Surge365.MassEmailReact.Infrastructure/Surge365.MassEmailReact.Infrastructure.csproj @@ -12,6 +12,7 @@ +