Enhance authentication features and refactor codebase

Updated `AuthenticationController` with new methods for authentication, token refresh, and password recovery. Improved error handling and response structure.

Refactored dependency injection in `Program.cs` and added JWT settings in `appsettings.json`. Removed unused `Class1.cs` files.

Introduced new DTOs for authentication requests and updated `IAuthService` and `IUserRepository` interfaces. Enhanced `User` class and added `AuthResult` enum for standardized responses.

Expanded `DataAccess` for better database operations and updated `UserRepository` and `AuthService` to implement new authentication logic.

Front-end changes include renaming variables for consistency and updating the `ForgotPasswordModal` and `Login.tsx` components to use usernames instead of email addresses.

Updated API proxy path in `vite.config.ts` and ensured proper typing for `API_BASE_URL` in global TypeScript definitions.
This commit is contained in:
David Headrick 2025-02-22 18:45:37 -06:00
parent 88bcac382c
commit b3f266f9a8
23 changed files with 919 additions and 151 deletions

View File

@ -1,30 +1,51 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces;
namespace Surge365.MassEmailReact.Server.Controllers
{
//[Route("api/[controller]")]
//[ApiController]
//public class AuthenticationController : ControllerBase
//{
// private readonly IAuthService _authService;
[Route("api/[controller]")]
[ApiController]
public class AuthenticationController : ControllerBase
{
private readonly IAuthService _authService;
// public AuthenticationController(IAuthService authService)
// {
// _authService = authService;
// }
public AuthenticationController(IAuthService authService)
{
_authService = authService;
}
// [HttpPost("login")]
// public IActionResult Authenticate([FromBody] LoginRequest request)
// {
// var token = _authService.Authenticate(request.Username, request.Password);
// if (token == null)
// {
// return Unauthorized(new { message = "Invalid credentials" });
// }
[HttpPost("authenticate")]
public async Task<IActionResult> Authenticate([FromBody] LoginRequest request)
{
var authResponse = await _authService.Authenticate(request.Username, request.Password);
if (!authResponse.authenticated)
return Unauthorized(new { message = authResponse.errorMessage });
else if(authResponse.token == null)
return Unauthorized(new { message = "Invalid credentials" });
// return Ok(new { token });
// }
//}
return Ok(new { success = true, authResponse.token.Value.accessToken });
}
[HttpPost("refreshtoken")]
public IActionResult RefreshToken([FromBody] RefreshTokenRequest request)
{
Guid? userId = Guid.NewGuid();//TODO: Lookup user in session
if (userId == null)
{
return Unauthorized("Invalid refresh token");
}
var tokens = _authService.GenerateTokens(userId.Value, request.RefreshToken);
if(tokens == null)
return Unauthorized();
return Ok(new { accessToken = tokens.Value.accessToken, refreshToken = tokens.Value.refreshToken });
}
[HttpPost("generatepasswordrecovery")]
public IActionResult GeneratePasswordRecovery([FromBody] GeneratePasswordRecoveryRequest request)
{
return Ok(new { });
}
}
}

View File

@ -1,3 +1,8 @@
using Microsoft.AspNetCore.Identity;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Infrastructure.Repositories;
using Surge365.MassEmailReact.Infrastructure.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
@ -5,7 +10,8 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IAuthService, AuthService>();
var app = builder.Build();
app.UseDefaultFiles();

View File

@ -5,5 +5,13 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Jwt": {
"Secret": "Z9R5aFml+eRMeb7tyf8N9wCq3tZpS/EM6nGqOxlXPtOw4cJ3zS1AByczrIlD5F9d"
},
"AppCode": "MassEmailReactApi",
"EnvironmentCode": "UAT",
"ConnectionStrings": {
"Marketing.ConnectionString": "data source=uat.surge365.com;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Application Name=##application_name##"
}
}

View File

@ -1,7 +0,0 @@
namespace Surge365.MassEmailReact.Application
{
public class Class1
{
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Application.DTOs
{
public class GeneratePasswordRecoveryRequest
{
public required string Username { get; set; }
}
}

View File

@ -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
{
public class LoginRequest
{
public required string Username { get; set; }
public required string Password { get; set; }
}
}

View File

@ -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
{
public class RefreshTokenRequest
{
public required string RefreshToken { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace Surge365.MassEmailReact.Application.Interfaces
{
public interface IAuthService
{
Task<(bool authenticated, (string accessToken, string refreshToken)? token, string errorMessage)> Authenticate(string username, string password);
(string accessToken, string refreshToken)? GenerateTokens(Guid userId, string refreshToken);
}
}

View File

@ -0,0 +1,15 @@
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);
bool Authenticate(Guid userId, string refreshToken);
}
}

View File

@ -1,7 +0,0 @@
namespace Surge365.MassEmailReact.Domain
{
public class Class1
{
}
}

View File

@ -0,0 +1,34 @@
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; }
private User(int userKey, Guid userId, string username, string? firstName, string? middleInitial, string? lastName, bool isActive)
{
UserKey = userKey;
UserId = userId;
Username = username;
Username = firstName ?? "";
MiddleInitial = middleInitial ?? "";
LastName = lastName ?? "";
IsActive = isActive;
}
public static User Create(int userKey, Guid userId, string username, string? firstName, string? middleInitial, string? lastName, bool isActive)
{
return new User(userKey, userId, username, firstName, middleInitial, lastName, isActive);
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Domain.Enums
{
public enum AuthResult
{
Success = 0,
LoginNotFound = 1,
LoginLocked = 2,
InvalidPassword = 3,
UnexpectedError = 4,
NotAllowed = 5
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Domain.Enums.Extensions
{
public static class AuthResultExtensions
{
public static string GetMessage(this AuthResult response)
{
return response switch
{
AuthResult.Success => "Login successful.",
AuthResult.InvalidPassword or AuthResult.LoginNotFound => "Invalid username and/or password.",
AuthResult.LoginLocked => "User has been locked out.",
AuthResult.NotAllowed => "User has not been granted access to this application.",
AuthResult.UnexpectedError or _ => "Unexpected error."
};
}
}
}

View File

@ -1,7 +0,0 @@
namespace Surge365.MassEmailReact.Infrastructure
{
public class Class1
{
}
}

View File

@ -0,0 +1,414 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure
{
public class DataAccess
{
private string GetConnectionString(string connectionStringName)
{
if (_configuration == null)
return "";
return _configuration[$"ConnectionStrings:{connectionStringName}"] ?? "";
}
private string GetAppCode()
{
if (_configuration == null)
return "";
return _configuration[$"AppCode"] ?? "";
}
public DataAccess(IConfiguration configuration, string connectionStringName)
{
_configuration = configuration;
_connectionString = GetConnectionString(connectionStringName).Replace("##application_code##", GetAppCode());
}
internal IConfiguration? _configuration;
internal Guid _sessionID = Guid.NewGuid();
internal string _connectionString;
internal int _timeout = 30;
internal SqlTransaction? _transaction;
internal SqlConnection? _connection;
internal bool _transactionStarted;
internal Object? _transactionStarter;
public string ConnectionString
{
get { return _connectionString; }
set { _connectionString = value; }
}
public int Timeout
{
get { return _timeout; }
set { _timeout = value; }
}
public bool TransactionStarted
{
get { return _transactionStarted; }
internal set { _transactionStarted = value; }
}
public Object? TransactionStarter
{
get { return _transactionStarter; }
internal set { _transactionStarter = value; }
}
#region Non-Async
internal void OpenConnection()
{
_connection = new SqlConnection(_connectionString);
_connection.Open();
}
internal void CloseConnection()
{
if (_connection == null)
return;
_connection.Close();
_connection.Dispose();
_connection = null;
}
public SqlTransaction BeginTransaction(Object sender)
{
if (_transaction != null)
return _transaction;
OpenConnection();
ArgumentNullException.ThrowIfNull(_connection);
_transaction = _connection.BeginTransaction();
_transactionStarted = true;
_transactionStarter = sender;
return _transaction;
}
public void RollbackTransaction(Object sender)
{
if (_transaction != null && sender == _transactionStarter)
{
_transaction.Rollback();
_transaction = null;
CloseConnection();
_transactionStarter = null;
_transactionStarted = false;
}
}
public void CommitTransaction(Object sender)
{
if (_transaction != null && sender == _transactionStarter)
{
_transaction.Commit();
_transaction = null;
CloseConnection();
_transactionStarter = null;
_transactionStarted = false;
}
}
public DataSet CallRetrievalProcedure(List<SqlParameter> parameters, string proc, int timeoutSeconds = 0)
{
DataSet ds = new DataSet();
bool createdConnection = false;
if (timeoutSeconds == 0)
timeoutSeconds = _timeout;
try
{
if (_connection == null)
{
createdConnection = true;
OpenConnection();
}
using SqlCommand cmd = new SqlCommand(proc, _connection);
cmd.Transaction = _transaction;
if (parameters != null)
{
foreach (SqlParameter p in parameters)
{
if (p != null)
cmd.Parameters.Add(p);
}
}
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = timeoutSeconds;
using SqlDataAdapter da = new SqlDataAdapter(cmd);
try
{
da.Fill(ds);
}
catch (Exception ex)
{
string message = String.Format("Unable to retrieve data from proc: {0}", proc);
LogError(ex, message, "CallRetrievalProcedure");
throw;
}
if (createdConnection)
{
CloseConnection();
}
}
catch (Exception)
{
if (createdConnection)
{
CloseConnection();
}
throw;
}
return ds;
}
public int CallActionProcedure(List<SqlParameter> parameters, string proc, int timeoutSeconds = 0)
{
int iReturnVal = -1;
if (timeoutSeconds == 0)
timeoutSeconds = _timeout;
bool createdConnection = false;
try
{
if (_connection == null)
{
createdConnection = true;
OpenConnection();
}
using SqlCommand cmd = new SqlCommand(proc, _connection);
cmd.Transaction = _transaction;
if (parameters != null)
{
foreach (SqlParameter p in parameters)
{
if (p != null)
cmd.Parameters.Add(p);
}
}
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = timeoutSeconds;
try
{
iReturnVal = cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
string message = String.Format("Unable to execute proc: {0}", proc);
LogError(ex, message, "CallActionProcedure");
throw;
}
}
catch (Exception)
{
if (createdConnection)
{
CloseConnection();
}
throw;
}
if (createdConnection)
{
CloseConnection();
}
return iReturnVal;
}
private static void LogError(Exception ex, string message, string task)
{
Exception ex1 = ex;
if (ex == null)
ex1 = new Exception(message);
else if (!string.IsNullOrEmpty(message))
ex1 = new Exception(message, ex);
//Log
}
#endregion
#region Async
internal async Task OpenConnectionAsync()
{
_connection = new SqlConnection(_connectionString);
await _connection.OpenAsync();
}
internal async Task CloseConnectionAsync()
{
if (_connection == null)
return;
await _connection.CloseAsync();
_connection.Dispose();
_connection = null;
}
public async Task<SqlTransaction> BeginTransactionAsync(Object sender)
{
if (_transaction != null)
return _transaction;
await OpenConnectionAsync();
ArgumentNullException.ThrowIfNull(_connection);
_transaction = _connection.BeginTransaction();
_transactionStarted = true;
_transactionStarter = sender;
return _transaction;
}
public async Task RollbackTransactionAsync(Object sender)
{
if (_transaction != null && sender == _transactionStarter)
{
await _transaction.RollbackAsync();
_transaction = null;
await CloseConnectionAsync();
_transactionStarter = null;
_transactionStarted = false;
}
}
public async Task CommitTransactionAsync(Object sender)
{
if (_transaction != null && sender == _transactionStarter)
{
await _transaction.CommitAsync();
_transaction = null;
await CloseConnectionAsync();
_transactionStarter = null;
_transactionStarted = false;
}
}
public async Task<DataSet> CallRetrievalProcedureAsync(List<SqlParameter> parameters, string proc, int timeoutSeconds = 0)
{
DataSet ds = new DataSet();
bool createdConnection = false;
if (timeoutSeconds == 0)
timeoutSeconds = _timeout;
try
{
if (_connection == null)
{
createdConnection = true;
await OpenConnectionAsync();
}
using SqlCommand cmd = new SqlCommand(proc, _connection);
cmd.Transaction = _transaction;
if (parameters != null)
{
foreach (SqlParameter p in parameters)
{
if (p != null)
cmd.Parameters.Add(p);
}
}
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = timeoutSeconds;
try
{
//using var reader = await cmd.ExecuteReaderAsync();
using var adapter = new SqlDataAdapter(cmd);
await Task.Run(() => adapter.Fill(ds));
}
catch (Exception ex)
{
string message = String.Format("Unable to retrieve data from proc: {0}", proc);
await LogErrorAsync(ex, message, "CallRetrievalProcedure");
throw;
}
if (createdConnection)
{
await CloseConnectionAsync();
}
}
catch (Exception)
{
if (createdConnection)
{
await CloseConnectionAsync();
}
throw;
}
return ds;
}
public async Task<int> CallActionProcedureAsync(List<SqlParameter> parameters, string proc, int timeoutSeconds = 0)
{
int iReturnVal = -1;
if (timeoutSeconds == 0)
timeoutSeconds = _timeout;
bool createdConnection = false;
try
{
if (_connection == null)
{
createdConnection = true;
await OpenConnectionAsync();
}
using SqlCommand cmd = new SqlCommand(proc, _connection);
cmd.Transaction = _transaction;
if (parameters != null)
{
foreach (SqlParameter p in parameters)
{
if (p != null)
cmd.Parameters.Add(p);
}
}
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = timeoutSeconds;
try
{
iReturnVal = await cmd.ExecuteNonQueryAsync();
}
catch (Exception ex)
{
string message = String.Format("Unable to execute proc: {0}", proc);
await LogErrorAsync(ex, message, "CallActionProcedure");
throw;
}
}
catch (Exception)
{
if (createdConnection)
{
await CloseConnectionAsync();
}
throw;
}
if (createdConnection)
{
await CloseConnectionAsync();
}
return iReturnVal;
}
private static async Task LogErrorAsync(Exception ex, string message, string task)
{
Exception ex1 = ex;
if (ex == null)
ex1 = new Exception(message);
else if (!string.IsNullOrEmpty(message))
ex1 = new Exception(message, ex);
try
{
using SqlConnection c = new SqlConnection();
await c.OpenAsync();
using SqlCommand cmd = new SqlCommand("lg_log_error", c);
await cmd.ExecuteNonQueryAsync();
}
catch (Exception iex) //Trap all errors, don't do any
{
Console.WriteLine($"Unhandled Exception occurred logging exception: exception={iex}; trying to log exception={ex}");
}
}
#endregion
}
}

View File

@ -0,0 +1,140 @@
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.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Repositories
{
public class UserRepository (IConfiguration config) : IUserRepository
{
private IConfiguration _config = config;
private const string _connectionStringName = "Marketing.ConnectionString";
//private static readonly List<User> Users = new();
public async Task<(User? user, string message)> Authenticate(string username, string password)
{
List<SqlParameter> pms = new List<SqlParameter>();
pms.Add(new SqlParameter("username", username));
pms.Add(new SqlParameter("password", password));
pms.Add(new SqlParameter("application_code", "MassEmailWeb")); //TODO: Pull from config
SqlParameter pmResponseNumber = new SqlParameter("response_number", SqlDbType.SmallInt);
pmResponseNumber.Direction = ParameterDirection.Output;
pms.Add(pmResponseNumber);
SqlParameter pUserKey = new SqlParameter("login_key", SqlDbType.Int);
pUserKey.Direction = ParameterDirection.Output;
pms.Add(pUserKey);
DataAccess da = new DataAccess(_config, _connectionStringName);
DataSet ds = await da.CallRetrievalProcedureAsync(pms, "adm_authenticate_login");
var result = (AuthResult)Convert.ToInt16(pmResponseNumber.Value);
string responseMessage = AuthResultExtensions.GetMessage(result);
if (result == AuthResult.Success)
{
if (ds.Tables.Count == 0 || ds.Tables[0].Rows.Count == 0)
return (null, "No user row returned");
return (LoadFromDataRow(ds.Tables[0].Rows[0]), responseMessage);
}
return (null, responseMessage);
}
public bool Authenticate(Guid userId, string refreshToken)
{
//TODO: Validate refresh token
return true;
}
public async Task<User?> GetByUsername(string username)
{
List<SqlParameter> pms = new List<SqlParameter>();
pms.Add(new SqlParameter("username", username));
DataAccess da = new DataAccess(_config, _connectionStringName);
DataSet ds = await da.CallRetrievalProcedureAsync(pms, "adm_get_login_by_username");
if (ds.Tables.Count == 0 || ds.Tables[0].Rows.Count == 0)
return null;
List<User> users = LoadFromDataRow(ds.Tables[0]);
return users.FirstOrDefault();
}
public async Task<User?> GetByKey(int userKey)
{
List<SqlParameter> pms = new List<SqlParameter>();
pms.Add(new SqlParameter("login_key", userKey));
DataAccess da = new DataAccess(_config, _connectionStringName);
DataSet ds = await da.CallRetrievalProcedureAsync(pms, "adm_get_adm_login_by_key");
if (ds.Tables.Count == 0 || ds.Tables[0].Rows.Count == 0)
return null;
List<User> users = LoadFromDataRow(ds.Tables[0]);
return users.FirstOrDefault();
}
public async Task<User?> GetById(Guid userId)
{
List<SqlParameter> pms = new List<SqlParameter>();
pms.Add(new SqlParameter("login_id", userId));
DataAccess da = new DataAccess(_config, _connectionStringName);
DataSet ds = await da.CallRetrievalProcedureAsync(pms, "adm_get_adm_login_by_id");
if (ds.Tables.Count == 0 || ds.Tables[0].Rows.Count == 0)
return null;
List<User> users = LoadFromDataRow(ds.Tables[0]);
return users.FirstOrDefault();
}
public async Task<List<User>> GetAll(bool activeOnly = true)
{
List<SqlParameter> pms = new List<SqlParameter>();
pms.Add(new SqlParameter("active_only", activeOnly));
DataAccess da = new DataAccess(_config, _connectionStringName);
DataSet ds = await da.CallRetrievalProcedureAsync(pms, "adm_get_adm_login_all");
if (ds.Tables.Count == 0 || ds.Tables[0].Rows.Count == 0)
return null;
return LoadFromDataRow(ds.Tables[0]);
}
//public void Add(User user)
//{
// Users.Add(user);
//}
private List<User> LoadFromDataRow(DataTable dt)
{
ArgumentNullException.ThrowIfNull(dt);
ArgumentNullException.ThrowIfNull(dt.Rows);
List<User> users = new List<User>();
foreach (DataRow dr in dt.Rows)
{
users.Add(LoadFromDataRow(dr));
}
return users;
}
private User LoadFromDataRow(DataRow dr)
{
ArgumentNullException.ThrowIfNull(dr);
return User.Create(dr.Field<int>("login_key"),
dr.Field<Guid>("login_id"),
dr.Field<string>("username")!,
dr.Field<string?>("first_name"),
dr.Field<string?>("middle_initial"),
dr.Field<string?>("last_name"),
dr.Field<bool>("is_active"));
}
}
}

View File

@ -0,0 +1,94 @@
using Surge365.MassEmailReact.Application.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
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;
namespace Surge365.MassEmailReact.Infrastructure.Services
{
public class AuthService : IAuthService
{
private const int TOKEN_MINUTES = 60;
private readonly IUserRepository _userRepository;
private readonly IConfiguration _config;
public AuthService(IUserRepository userRepository, IConfiguration config)
{
_userRepository = userRepository;
_config = config;
}
public async Task<(bool authenticated, (string accessToken, string refreshToken)? token, 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 tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, authResponse.user.UserId.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(ClaimTypes.Role, "User")
}),
Expires = DateTime.UtcNow.AddMinutes(TOKEN_MINUTES),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature
)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var accessToken = tokenHandler.WriteToken(token);
var refreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
//_userRepository.SaveRefreshToken(userId.Value, refreshToken); // TODO: Store refresh token in DB
return (true, (accessToken, refreshToken), "");
}
public (string accessToken, string refreshToken)? GenerateTokens(Guid userId, string refreshToken)
{
if (!_userRepository.Authenticate(userId, refreshToken))
{
return null;
}
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!);
var username = "";
//TODO: Look update User
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()!),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(ClaimTypes.Role, "User")
}),
Expires = DateTime.UtcNow.AddMinutes(TOKEN_MINUTES),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature
)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var accessToken = tokenHandler.WriteToken(token);
var newRefreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
//_userRepository.SaveRefreshToken(userId.Value, newRefreshToken); // TODO: Store refresh token in DB
return (accessToken, newRefreshToken);
}
}
}

View File

@ -11,4 +11,13 @@
<ProjectReference Include="..\Surge365.MassEmailReact.Domain\Surge365.MassEmailReact.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.5.0" />
</ItemGroup>
</Project>

View File

@ -51,8 +51,8 @@ function checkLoggedInStatus() {
// $("#spanMenuName").html(user.FirstName + ' ' + user.LastName);
// var userid = $.localStorage("session_currentUser").UserID;
// var imgid = userid + "&t=" + new Date().getTime();
// var userId = $.localStorage("session_currentUser").userId;
// var imgid = userId + "&t=" + new Date().getTime();
/*
if (user.HasProfileImage) {
$("#imgRightProfile").attr("src", "/webservices/getprofileimage.ashx?id=" + imgid);
@ -391,7 +391,7 @@ function formatSelect2DataForUser(id, data) {
for (var x = 0; x < data.length; x++) {
var obj = {};
obj.id = data[x][id];
obj.text = data[x]["FirstName"] + ' ' + data[x]["LastName"] + ' - ' + data[x]["UserID"];
obj.text = data[x]["FirstName"] + ' ' + data[x]["LastName"] + ' - ' + data[x]["userId"];
newdata.push(obj);
}
@ -426,7 +426,7 @@ function fillLocationsDropDown(deferred) {
var search = {};
if (!$.userHasRole("Administrators")) {
search.UserID = $.localStorage("session_currentUser").UserID;
search.userId = $.localStorage("session_currentUser").userId;
}
else {
search = null;
@ -531,7 +531,7 @@ function fillEmployeesDropDown(deferred, dropdown) {
$.sessionStorage("session_users", json.data);
if ($.getBoolean(json.success)) {
var data = formatSelect2DataForUser("UserID", $.sessionStorage("session_users"));
var data = formatSelect2DataForUser("userId", $.sessionStorage("session_users"));
dropdown.select2({
placeholder: "Select an employee...",
allowClear: true,
@ -601,7 +601,7 @@ function growlSuccess(msg) {
function getUser(users, id) {
for (var x = 0; x < users.length; x++) {
if (users[x].UserID == id) {
if (users[x].userId == id) {
return users[x];
break;
}

View File

@ -1,118 +1,84 @@
import { useState } from 'react';
import { useState, FormEvent } from 'react';
import { Modal, Button, Form } from 'react-bootstrap';
import { FaExclamationCircle } from 'react-icons/fa'; // For optional font icon
import PropTypes from 'prop-types';
import utils from '@/ts/utils';
import utils from '@/ts/utils.ts'
type FormErrors = Record<string, string>;
const ForgotPasswordModal = ({ show, onClose }) => {
const [email, setEmail] = useState('');
const [formErrors, setFormErrors] = useState({});
const [emailNotFound, setEmailNotFound] = useState(false);
type ForgotPasswordModalProps = {
show: boolean;
onClose: () => void;
};
const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose }) => {
const [username, setUsername] = useState('');
const [formErrors, setFormErrors] = useState<FormErrors>({});
const [usernameNotFound, setUsernameNotFound] = useState(false);
const [recoveryStarted, setRecoveryStarted] = useState(false);
const validate = () => {
const validate = (): boolean => {
setFormErrors({});
const errors = {};
if (!email.trim()) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(email)) {
errors.email = 'Invalid email address';
const errors: FormErrors = {};
if (!username.trim()) {
errors.username = 'Username is required';
}
if (Object.keys(errors).length > 0) {
setFormErrors(errors);
return false;
} else {
return true;
}
}
};
const handleStartPasswordRecovery = async (e) => {
const handleStartPasswordRecovery = async (e: FormEvent) => {
e.preventDefault();
setEmailNotFound(false);
setUsernameNotFound(false);
if (validate()) {
console.log('Processing forgot password for', email);
console.log('Processing forgot password for', username);
await utils.webMethod({
'methodPage': 'UserMethods',
'methodName': 'GeneratePasswordRecovery',
'parameters': { "emailAddress": email },
success: function (json) {
methodPage: 'authenticate',
methodName: 'generatepasswordrecovery',
parameters: { username },
success: (json: any) => {
if (utils.getBoolean(json.success)) {
setRecoveryStarted(true);
} else {
setUsernameNotFound(true);
}
else {
setEmailNotFound(true);
}
}
},
});
}
};
/*
$("#btnResetPassword").click(function (e) {
$("#frmResetPassword").validator('validate');
e.preventDefault();
var isValid = !$('#frmResetPassword .has-error').length
if (isValid) {
$.webMethod({
'methodPage': 'UserMethods',
'methodName': 'ResetPasswordFromToken',
'parameters': { "token": getParameterByName("guid"), "password": $("#txtNewPassword").val() },
success: function (json) {
if ($.getBoolean(json.success)) {
//redirect user to Dashboard
location.href = "/vehicles";
}
else {
//error
}
}
});
}
})
*/
return (
<Modal show={show} onHide={onClose} backdrop="static" keyboard={false} centered={true} animation={false} >
<Modal show={show} onHide={onClose} backdrop="static" keyboard={false} centered animation={false}>
<Modal.Header closeButton>
<Modal.Title>Forgot your password?</Modal.Title>
</Modal.Header>
<Modal.Body>
{emailNotFound && (
<span>An email has been sent to the address you provided. Please follow the instructions in the email in order to reset your password.</span>
{usernameNotFound && (
<span>An email has been sent to the address you provided. Please follow the instructions to reset your password.</span>
)}
{!recoveryStarted && (
<Form onSubmit={handleStartPasswordRecovery}>
<Form.Group controlId="formForgotEmail" className="position-relative mb-3">
<Form.Label className="mb-4 text-center">Enter your email address below and we&apos;ll send you instructions on how to reset your password...</Form.Label>
<Form.Label className="visually-hidden">Email Addresss</Form.Label>
<Form.Label className="mb-4 text-center">Enter your email address below and we'll send you instructions on how to reset your password...</Form.Label>
<Form.Label className="visually-hidden">Email</Form.Label>
<Form.Control
type="email"
placeholder="Email address"
value={email}
isInvalid={!!formErrors.email} // Add Bootstrap's invalid styling
onChange={(e) => setEmail(e.target.value)}
type="username"
placeholder="Username"
value={username}
isInvalid={!!formErrors.username}
onChange={(e) => setUsername(e.target.value)}
required
autoFocus
size="lg"
/>
{/* Validation Icon */}
{formErrors.email && (
<FaExclamationCircle
className="validation-icon text-danger"
title={formErrors.email}
/>
{formErrors.username && (
<FaExclamationCircle className="validation-icon text-danger" title={formErrors.username} />
)}
{/* Validation Message */}
<Form.Control.Feedback type="invalid">{formErrors.email}</Form.Control.Feedback>
<Form.Control.Feedback type="invalid">{formErrors.username}</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" className="bg-orange btn-flat w-100" type="submit">
Submit
@ -121,7 +87,7 @@ const ForgotPasswordModal = ({ show, onClose }) => {
)}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onClose} type="button">
<Button variant="secondary" onClick={onClose}>
Close
</Button>
</Modal.Footer>
@ -129,9 +95,5 @@ const ForgotPasswordModal = ({ show, onClose }) => {
);
};
ForgotPasswordModal.propTypes = {
show: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
};
export default ForgotPasswordModal;

View File

@ -12,7 +12,7 @@ function Login() {
const [isLoading, setIsLoading] = useState(false);
const [spinners, setSpinnersState] = useState<SpinnerState>({});
const [formErrors, setFormErrors] = useState<FormErrors>({});
const [email, setEmail] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false);
const [user, setUser] = useState<any>(null);
@ -40,17 +40,14 @@ function Login() {
setShowForgotPasswordModal(false);
};
const handleEmailBlur = () => {
};
const validateLoginForm = () => {
setFormErrors({});
const errors: FormErrors = {};
if (!email.trim()) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(email)) {
errors.email = 'Invalid email address';
if (!username.trim()) {
errors.username = 'Username is required';
//} else if (!/\S+@\S+\.\S+/.test(email)) {
// errors.email = 'Invalid email address';
}
if (!password.trim()) {
errors.password = 'Password is required';
@ -75,9 +72,9 @@ function Login() {
let loggedInUser: any = null;
await utils.webMethod({
methodPage: 'UserMethods',
methodName: 'AuthenticateUser',
parameters: { emailAddress: email, password: password },
methodPage: 'authentication',
methodName: 'authenticate',
parameters: { username: username, password: password },
success: (json: any) => {
if (utils.getBoolean(json.success)) {
loggedInUser = json.data;
@ -132,14 +129,13 @@ function Login() {
{loginError && (
<Form.Label style={{ color: 'red' }}>Login error</Form.Label>
)}
<Form.Group className="mb-3" controlId="txtEmail">
<Form.Label className="visually-hidden">Email address</Form.Label>
<Form.Group className="mb-3" controlId="txtUsernamel">
<Form.Label className="visually-hidden">Username</Form.Label>
<Form.Control
type="email"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={handleEmailBlur}
type="username"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
autoFocus
size="sm"

View File

@ -9,7 +9,7 @@ export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
// Add global values to window object for use in static JS
declare global {
interface Window {
API_BASE_URL: object
API_BASE_URL: string
}
}
if (typeof window !== 'undefined') {

View File

@ -47,7 +47,7 @@ export default defineConfig({
},
server: {
proxy: {
'^/weatherforecast': {
'^/api': {
target,
secure: false
}