mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 06:27:37 +00:00
changed login flow to support 2 phase program selection login.
This commit is contained in:
@@ -21,13 +21,12 @@ public class AuthController : BaseController
|
||||
_tokenService = tokenService;
|
||||
}
|
||||
|
||||
// Phase 1: verify credentials, return session token + list of programs
|
||||
[HttpPost("Login")]
|
||||
[ProducesResponseType(typeof(ResponseResult<LoginResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ResponseResult<LoginResponse>), StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ResponseResult<LoginResponse>>> Login([FromBody] LoginDto login)
|
||||
{
|
||||
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(login.Email) || string.IsNullOrWhiteSpace(login.Password))
|
||||
{
|
||||
return BadRequest(new ResponseResult<LoginResponse>
|
||||
@@ -74,21 +73,108 @@ public class AuthController : BaseController
|
||||
});
|
||||
}
|
||||
|
||||
// Generate JWT access token
|
||||
var accessToken = _tokenService.GenerateToken(user.IdUser, user.Email!, user.RoleInternalName);
|
||||
var programs = await _userRepo.GetProgramsForUserIdAsync(user.IdUser);
|
||||
var programList = programs.ToList();
|
||||
|
||||
// Generate refresh token secret
|
||||
var secretToken = Guid.NewGuid().ToString();
|
||||
var (refreshTokenHash, refreshTokenSalt) = PasswordHasher.HashPassword(secretToken);
|
||||
if (programList.Count == 0)
|
||||
{
|
||||
return Ok(new ResponseResult<LoginResponse>
|
||||
{
|
||||
Success = false,
|
||||
Message = "No active programs found for this account."
|
||||
});
|
||||
}
|
||||
|
||||
// Get device info
|
||||
var sessionToken = _tokenService.GenerateSessionToken(user.IdUser, user.Email!);
|
||||
|
||||
return Ok(new ResponseResult<LoginResponse>
|
||||
{
|
||||
Success = true,
|
||||
Message = "Login successful.",
|
||||
Data = new LoginResponse
|
||||
{
|
||||
SessionToken = sessionToken,
|
||||
Programs = programList.Select(p => new UserProgramSummary
|
||||
{
|
||||
ProgramId = p.IdProgram,
|
||||
ProgramName = p.ProgramName!,
|
||||
Role = p.RoleInternalName,
|
||||
RoleDisplayName = p.RoleDisplayName,
|
||||
IsPrimary = p.IsPrimary
|
||||
}).ToList()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 2: user selects a program, receive program-scoped JWT + refresh token
|
||||
// Requires the phase 1 session token in the Authorization: Bearer header
|
||||
[HttpPost("SelectProgram")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(ResponseResult<SelectProgramResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ResponseResult<SelectProgramResponse>), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ResponseResult<SelectProgramResponse>), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<ResponseResult<SelectProgramResponse>>> SelectProgram([FromBody] SelectProgramDto dto)
|
||||
{
|
||||
var authStage = User.FindFirst("auth_stage")?.Value;
|
||||
if (authStage != "selecting_program")
|
||||
{
|
||||
return Unauthorized(new ResponseResult<SelectProgramResponse>
|
||||
{
|
||||
Success = false,
|
||||
Message = "A session token is required to select a program."
|
||||
});
|
||||
}
|
||||
|
||||
var (userId, userIdError) = GetUserIdFromClaims();
|
||||
if (userIdError != null) return userIdError;
|
||||
|
||||
if (!Guid.TryParse(dto.ProgramId, out Guid programId))
|
||||
{
|
||||
return BadRequest(new ResponseResult<SelectProgramResponse>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Invalid program ID format."
|
||||
});
|
||||
}
|
||||
|
||||
var programUser = await _userRepo.GetByIdWithProgramAsync(userId, programId);
|
||||
if (programUser == null)
|
||||
{
|
||||
return Unauthorized(new ResponseResult<SelectProgramResponse>
|
||||
{
|
||||
Success = false,
|
||||
Message = "User does not have access to this program."
|
||||
});
|
||||
}
|
||||
|
||||
if (programUser.Status != "active")
|
||||
{
|
||||
return Unauthorized(new ResponseResult<SelectProgramResponse>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Access to this program is inactive."
|
||||
});
|
||||
}
|
||||
|
||||
var accessToken = _tokenService.GenerateToken(
|
||||
programUser.IdUser,
|
||||
programUser.Email!,
|
||||
programUser.RoleInternalName,
|
||||
programUser.IdProgram);
|
||||
|
||||
var jwtExpiresIn = _tokenService.GetTokenExpiryInSeconds(accessToken);
|
||||
|
||||
var refreshToken = Guid.NewGuid().ToString();
|
||||
var (refreshTokenHash, refreshTokenSalt) = PasswordHasher.HashPassword(refreshToken);
|
||||
|
||||
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
var userAgent = HttpContext.Request.Headers.UserAgent.ToString();
|
||||
var deviceInfo = JsonSerializer.Serialize(new { ip_address = ipAddress });
|
||||
|
||||
// Store refresh token in database (30 days expiration)
|
||||
var refreshTokenId = await _authRepo.CreateRefreshTokenAsync(
|
||||
Guid.NewGuid(),
|
||||
user.IdUser,
|
||||
programUser.IdUser,
|
||||
programUser.IdProgram,
|
||||
refreshTokenHash,
|
||||
refreshTokenSalt,
|
||||
expiresInSeconds: 2592000, // 30 days
|
||||
@@ -98,28 +184,29 @@ public class AuthController : BaseController
|
||||
|
||||
if (!refreshTokenId.HasValue)
|
||||
{
|
||||
return Ok(new ResponseResult<LoginResponse>
|
||||
return Ok(new ResponseResult<SelectProgramResponse>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Failed to create refresh token."
|
||||
});
|
||||
}
|
||||
|
||||
// Build full refresh token: {id}.{secret}
|
||||
var fullRefreshToken = $"{refreshTokenId.Value}.{secretToken}";
|
||||
var fullRefreshToken = $"{refreshTokenId.Value}.{refreshToken}";
|
||||
|
||||
return Ok(new ResponseResult<LoginResponse>
|
||||
return Ok(new ResponseResult<SelectProgramResponse>
|
||||
{
|
||||
Success = true,
|
||||
Message = "Login successful.",
|
||||
Data = new LoginResponse
|
||||
Message = "Program selected.",
|
||||
Data = new SelectProgramResponse
|
||||
{
|
||||
UserId = user.IdUser,
|
||||
Email = user.Email!,
|
||||
UserId = programUser.IdUser,
|
||||
Email = programUser.Email!,
|
||||
ProgramName = programUser.ProgramName!,
|
||||
Jwt = accessToken,
|
||||
RefreshToken = fullRefreshToken,
|
||||
Role = user.RoleInternalName,
|
||||
RoleDisplayName = user.RoleDisplayName
|
||||
Role = programUser.RoleInternalName,
|
||||
RoleDisplayName = programUser.RoleDisplayName,
|
||||
JwtExpiresIn = jwtExpiresIn
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -139,7 +226,6 @@ public class AuthController : BaseController
|
||||
});
|
||||
}
|
||||
|
||||
// Split token into ID and secret: {id}.{secret}
|
||||
var dotIndex = refreshTokenDto.RefreshToken.IndexOf('.');
|
||||
if (dotIndex < 1)
|
||||
{
|
||||
@@ -199,21 +285,25 @@ public class AuthController : BaseController
|
||||
});
|
||||
}
|
||||
|
||||
var tokenUser = await _userRepo.GetByIdAsync(matchedToken.IdUser);
|
||||
if (tokenUser == null)
|
||||
// Use program-scoped lookup so the new JWT carries current role + program
|
||||
var programUser = await _userRepo.GetByIdWithProgramAsync(matchedToken.IdUser, matchedToken.IdProgram);
|
||||
if (programUser == null)
|
||||
{
|
||||
return Unauthorized(new ResponseResult<TokenRefreshResponse>
|
||||
{
|
||||
Success = false,
|
||||
Message = "User not found."
|
||||
Message = "User or program not found."
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new JWT
|
||||
var newJwtToken = _tokenService.GenerateToken(tokenUser.IdUser, tokenUser.Email!, tokenUser.RoleInternalName);
|
||||
var newJwtToken = _tokenService.GenerateToken(
|
||||
programUser.IdUser,
|
||||
programUser.Email!,
|
||||
programUser.RoleInternalName,
|
||||
programUser.IdProgram);
|
||||
|
||||
var jwtExpiresIn = _tokenService.GetTokenExpiryInSeconds(newJwtToken);
|
||||
|
||||
// Generate new refresh token (rotation)
|
||||
var newSecretToken = Guid.NewGuid().ToString();
|
||||
var (newRefreshTokenHash, newRefreshTokenSalt) = PasswordHasher.HashPassword(newSecretToken);
|
||||
|
||||
@@ -224,7 +314,8 @@ public class AuthController : BaseController
|
||||
var newRefreshTokenId = await _authRepo.ReplaceRefreshTokenAsync(
|
||||
matchedToken.IdRefreshToken,
|
||||
Guid.NewGuid(),
|
||||
tokenUser.IdUser,
|
||||
programUser.IdUser,
|
||||
programUser.IdProgram,
|
||||
newRefreshTokenHash,
|
||||
newRefreshTokenSalt,
|
||||
expiresInSeconds: 2592000,
|
||||
|
||||
Reference in New Issue
Block a user