mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 02:57:36 +00:00
changed login flow to support 2 phase program selection login.
This commit is contained in:
@@ -39,4 +39,16 @@ public class BaseController : ControllerBase
|
|||||||
{
|
{
|
||||||
return roles.Any(User.IsInRole);
|
return roles.Any(User.IsInRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected (Guid programId, ActionResult? error) GetProgramIdFromClaims()
|
||||||
|
{
|
||||||
|
var programIdClaim = User.FindFirst("program_id")?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(programIdClaim) || !Guid.TryParse(programIdClaim, out var programId))
|
||||||
|
{
|
||||||
|
return (Guid.Empty, Unauthorized("Missing or invalid program_id claim."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (programId, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ public class AuthController : BaseController
|
|||||||
_tokenService = tokenService;
|
_tokenService = tokenService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 1: verify credentials, return session token + list of programs
|
||||||
[HttpPost("Login")]
|
[HttpPost("Login")]
|
||||||
[ProducesResponseType(typeof(ResponseResult<LoginResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ResponseResult<LoginResponse>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(ResponseResult<LoginResponse>), StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ResponseResult<LoginResponse>), StatusCodes.Status400BadRequest)]
|
||||||
public async Task<ActionResult<ResponseResult<LoginResponse>>> Login([FromBody] LoginDto login)
|
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))
|
if (string.IsNullOrWhiteSpace(login.Email) || string.IsNullOrWhiteSpace(login.Password))
|
||||||
{
|
{
|
||||||
return BadRequest(new ResponseResult<LoginResponse>
|
return BadRequest(new ResponseResult<LoginResponse>
|
||||||
@@ -74,21 +73,108 @@ public class AuthController : BaseController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT access token
|
var programs = await _userRepo.GetProgramsForUserIdAsync(user.IdUser);
|
||||||
var accessToken = _tokenService.GenerateToken(user.IdUser, user.Email!, user.RoleInternalName);
|
var programList = programs.ToList();
|
||||||
|
|
||||||
// Generate refresh token secret
|
if (programList.Count == 0)
|
||||||
var secretToken = Guid.NewGuid().ToString();
|
{
|
||||||
var (refreshTokenHash, refreshTokenSalt) = PasswordHasher.HashPassword(secretToken);
|
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 userAgent = HttpContext.Request.Headers.UserAgent.ToString();
|
||||||
var deviceInfo = JsonSerializer.Serialize(new { ip_address = ipAddress });
|
var deviceInfo = JsonSerializer.Serialize(new { ip_address = ipAddress });
|
||||||
|
|
||||||
// Store refresh token in database (30 days expiration)
|
|
||||||
var refreshTokenId = await _authRepo.CreateRefreshTokenAsync(
|
var refreshTokenId = await _authRepo.CreateRefreshTokenAsync(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
user.IdUser,
|
programUser.IdUser,
|
||||||
|
programUser.IdProgram,
|
||||||
refreshTokenHash,
|
refreshTokenHash,
|
||||||
refreshTokenSalt,
|
refreshTokenSalt,
|
||||||
expiresInSeconds: 2592000, // 30 days
|
expiresInSeconds: 2592000, // 30 days
|
||||||
@@ -98,28 +184,29 @@ public class AuthController : BaseController
|
|||||||
|
|
||||||
if (!refreshTokenId.HasValue)
|
if (!refreshTokenId.HasValue)
|
||||||
{
|
{
|
||||||
return Ok(new ResponseResult<LoginResponse>
|
return Ok(new ResponseResult<SelectProgramResponse>
|
||||||
{
|
{
|
||||||
Success = false,
|
Success = false,
|
||||||
Message = "Failed to create refresh token."
|
Message = "Failed to create refresh token."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build full refresh token: {id}.{secret}
|
var fullRefreshToken = $"{refreshTokenId.Value}.{refreshToken}";
|
||||||
var fullRefreshToken = $"{refreshTokenId.Value}.{secretToken}";
|
|
||||||
|
|
||||||
return Ok(new ResponseResult<LoginResponse>
|
return Ok(new ResponseResult<SelectProgramResponse>
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = true,
|
||||||
Message = "Login successful.",
|
Message = "Program selected.",
|
||||||
Data = new LoginResponse
|
Data = new SelectProgramResponse
|
||||||
{
|
{
|
||||||
UserId = user.IdUser,
|
UserId = programUser.IdUser,
|
||||||
Email = user.Email!,
|
Email = programUser.Email!,
|
||||||
|
ProgramName = programUser.ProgramName!,
|
||||||
Jwt = accessToken,
|
Jwt = accessToken,
|
||||||
RefreshToken = fullRefreshToken,
|
RefreshToken = fullRefreshToken,
|
||||||
Role = user.RoleInternalName,
|
Role = programUser.RoleInternalName,
|
||||||
RoleDisplayName = user.RoleDisplayName
|
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('.');
|
var dotIndex = refreshTokenDto.RefreshToken.IndexOf('.');
|
||||||
if (dotIndex < 1)
|
if (dotIndex < 1)
|
||||||
{
|
{
|
||||||
@@ -199,21 +285,25 @@ public class AuthController : BaseController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenUser = await _userRepo.GetByIdAsync(matchedToken.IdUser);
|
// Use program-scoped lookup so the new JWT carries current role + program
|
||||||
if (tokenUser == null)
|
var programUser = await _userRepo.GetByIdWithProgramAsync(matchedToken.IdUser, matchedToken.IdProgram);
|
||||||
|
if (programUser == null)
|
||||||
{
|
{
|
||||||
return Unauthorized(new ResponseResult<TokenRefreshResponse>
|
return Unauthorized(new ResponseResult<TokenRefreshResponse>
|
||||||
{
|
{
|
||||||
Success = false,
|
Success = false,
|
||||||
Message = "User not found."
|
Message = "User or program not found."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new JWT
|
var newJwtToken = _tokenService.GenerateToken(
|
||||||
var newJwtToken = _tokenService.GenerateToken(tokenUser.IdUser, tokenUser.Email!, tokenUser.RoleInternalName);
|
programUser.IdUser,
|
||||||
|
programUser.Email!,
|
||||||
|
programUser.RoleInternalName,
|
||||||
|
programUser.IdProgram);
|
||||||
|
|
||||||
var jwtExpiresIn = _tokenService.GetTokenExpiryInSeconds(newJwtToken);
|
var jwtExpiresIn = _tokenService.GetTokenExpiryInSeconds(newJwtToken);
|
||||||
|
|
||||||
// Generate new refresh token (rotation)
|
|
||||||
var newSecretToken = Guid.NewGuid().ToString();
|
var newSecretToken = Guid.NewGuid().ToString();
|
||||||
var (newRefreshTokenHash, newRefreshTokenSalt) = PasswordHasher.HashPassword(newSecretToken);
|
var (newRefreshTokenHash, newRefreshTokenSalt) = PasswordHasher.HashPassword(newSecretToken);
|
||||||
|
|
||||||
@@ -224,7 +314,8 @@ public class AuthController : BaseController
|
|||||||
var newRefreshTokenId = await _authRepo.ReplaceRefreshTokenAsync(
|
var newRefreshTokenId = await _authRepo.ReplaceRefreshTokenAsync(
|
||||||
matchedToken.IdRefreshToken,
|
matchedToken.IdRefreshToken,
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
tokenUser.IdUser,
|
programUser.IdUser,
|
||||||
|
programUser.IdProgram,
|
||||||
newRefreshTokenHash,
|
newRefreshTokenHash,
|
||||||
newRefreshTokenSalt,
|
newRefreshTokenSalt,
|
||||||
expiresInSeconds: 2592000,
|
expiresInSeconds: 2592000,
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace WinStudentGoalTracker.DataAccess;
|
||||||
|
|
||||||
|
public class SelectProgramDto
|
||||||
|
{
|
||||||
|
public required string ProgramId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace WinStudentGoalTracker.DataAccess;
|
||||||
|
|
||||||
|
public class dbProgramUser
|
||||||
|
{
|
||||||
|
public required Guid IdUser { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public required Guid IdProgram { get; set; }
|
||||||
|
public string? ProgramName { get; set; }
|
||||||
|
public required string RoleInternalName { get; set; }
|
||||||
|
public required string RoleDisplayName { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ public class dbRefreshToken
|
|||||||
{
|
{
|
||||||
public Guid IdRefreshToken { get; set; }
|
public Guid IdRefreshToken { get; set; }
|
||||||
public Guid IdUser { get; set; }
|
public Guid IdUser { get; set; }
|
||||||
|
public Guid IdProgram { get; set; }
|
||||||
public required string TokenHash { get; set; }
|
public required string TokenHash { get; set; }
|
||||||
public required string TokenSalt { get; set; }
|
public required string TokenSalt { get; set; }
|
||||||
public DateTime ExpiresAt { get; set; }
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ namespace WinStudentGoalTracker.DataAccess;
|
|||||||
public class dbUser
|
public class dbUser
|
||||||
{
|
{
|
||||||
public required Guid IdUser { get; set; }
|
public required Guid IdUser { get; set; }
|
||||||
public Guid? IdRole { get; set; }
|
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public string? PasswordHash { get; set; }
|
public string? PasswordHash { get; set; }
|
||||||
@@ -11,6 +10,4 @@ public class dbUser
|
|||||||
public int FailedLoginAttempts { get; set; }
|
public int FailedLoginAttempts { get; set; }
|
||||||
public DateTime? LockedUntil { get; set; }
|
public DateTime? LockedUntil { get; set; }
|
||||||
public DateTime? CreatedAt { get; set; }
|
public DateTime? CreatedAt { get; set; }
|
||||||
public required string RoleInternalName { get; set; }
|
|
||||||
public required string RoleDisplayName { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace WinStudentGoalTracker.DataAccess;
|
||||||
|
|
||||||
|
public class dbUserProgram
|
||||||
|
{
|
||||||
|
public required Guid IdProgram { get; set; }
|
||||||
|
public string? ProgramName { get; set; }
|
||||||
|
public required string RoleInternalName { get; set; }
|
||||||
|
public required string RoleDisplayName { get; set; }
|
||||||
|
public bool IsPrimary { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ public class AuthRepository
|
|||||||
public async Task<Guid?> CreateRefreshTokenAsync(
|
public async Task<Guid?> CreateRefreshTokenAsync(
|
||||||
Guid refreshTokenId,
|
Guid refreshTokenId,
|
||||||
Guid userId,
|
Guid userId,
|
||||||
|
Guid programId,
|
||||||
string tokenHash,
|
string tokenHash,
|
||||||
string tokenSalt,
|
string tokenSalt,
|
||||||
int expiresInSeconds,
|
int expiresInSeconds,
|
||||||
@@ -24,6 +25,7 @@ public class AuthRepository
|
|||||||
{
|
{
|
||||||
p_id_refresh_token = refreshTokenId.ToString(),
|
p_id_refresh_token = refreshTokenId.ToString(),
|
||||||
p_id_user = userId.ToString(),
|
p_id_user = userId.ToString(),
|
||||||
|
p_id_program = programId.ToString(),
|
||||||
p_token_hash = tokenHash,
|
p_token_hash = tokenHash,
|
||||||
p_token_salt = tokenSalt,
|
p_token_salt = tokenSalt,
|
||||||
p_expires_in_seconds = expiresInSeconds,
|
p_expires_in_seconds = expiresInSeconds,
|
||||||
@@ -57,6 +59,7 @@ public class AuthRepository
|
|||||||
Guid oldTokenId,
|
Guid oldTokenId,
|
||||||
Guid newTokenId,
|
Guid newTokenId,
|
||||||
Guid userId,
|
Guid userId,
|
||||||
|
Guid programId,
|
||||||
string tokenHash,
|
string tokenHash,
|
||||||
string tokenSalt,
|
string tokenSalt,
|
||||||
int expiresInSeconds,
|
int expiresInSeconds,
|
||||||
@@ -71,6 +74,7 @@ public class AuthRepository
|
|||||||
p_old_token_id = oldTokenId.ToString(),
|
p_old_token_id = oldTokenId.ToString(),
|
||||||
p_id_refresh_token = newTokenId.ToString(),
|
p_id_refresh_token = newTokenId.ToString(),
|
||||||
p_id_user = userId.ToString(),
|
p_id_user = userId.ToString(),
|
||||||
|
p_id_program = programId.ToString(),
|
||||||
p_token_hash = tokenHash,
|
p_token_hash = tokenHash,
|
||||||
p_token_salt = tokenSalt,
|
p_token_salt = tokenSalt,
|
||||||
p_expires_in_seconds = expiresInSeconds,
|
p_expires_in_seconds = expiresInSeconds,
|
||||||
|
|||||||
@@ -25,4 +25,22 @@ public class UserRepository
|
|||||||
new { p_id_user = idUser.ToString() },
|
new { p_id_user = idUser.ToString() },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<dbProgramUser?> GetByIdWithProgramAsync(Guid idUser, Guid idProgram)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
return await db.QuerySingleOrDefaultAsync<dbProgramUser>(
|
||||||
|
"sp_User_GetById_WithProgram",
|
||||||
|
new { p_id_user = idUser.ToString(), p_id_program = idProgram.ToString() },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<dbUserProgram>> GetProgramsForUserIdAsync(Guid idUser)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
return await db.QueryAsync<dbUserProgram>(
|
||||||
|
"sp_UserPrograms_GetByUserId",
|
||||||
|
new { p_id_user = idUser.ToString() },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ namespace WinStudentGoalTracker.Models;
|
|||||||
|
|
||||||
public class LoginResponse
|
public class LoginResponse
|
||||||
{
|
{
|
||||||
public Guid UserId { get; set; }
|
public required string SessionToken { get; set; }
|
||||||
public required string Email { get; set; }
|
public required List<UserProgramSummary> Programs { get; set; }
|
||||||
public required string Jwt { get; set; }
|
}
|
||||||
public required string RefreshToken { get; set; }
|
|
||||||
public string? Role { get; set; }
|
public class UserProgramSummary
|
||||||
public string? RoleDisplayName { get; set; }
|
{
|
||||||
|
public Guid ProgramId { get; set; }
|
||||||
|
public required string ProgramName { get; set; }
|
||||||
|
public required string Role { get; set; }
|
||||||
|
public required string RoleDisplayName { get; set; }
|
||||||
|
public bool IsPrimary { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace WinStudentGoalTracker.Models;
|
||||||
|
|
||||||
|
public class SelectProgramResponse
|
||||||
|
{
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public required string Email { get; set; }
|
||||||
|
public required string ProgramName { get; set; }
|
||||||
|
public required string Jwt { get; set; }
|
||||||
|
public required string RefreshToken { get; set; }
|
||||||
|
public required string Role { get; set; }
|
||||||
|
public required string RoleDisplayName { get; set; }
|
||||||
|
public int JwtExpiresIn { get; set; }
|
||||||
|
}
|
||||||
@@ -10,15 +10,42 @@ public class TokenService
|
|||||||
{
|
{
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
private readonly int _tokenExpiryInSeconds = 60 * 15; // 15 minutes
|
private readonly int _tokenExpiryInSeconds = 60 * 15; // 15 minutes
|
||||||
|
private readonly int _sessionTokenExpiryInSeconds = 60 * 5; // 5 minutes
|
||||||
|
|
||||||
public TokenService(IConfiguration config)
|
public TokenService(IConfiguration config)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GenerateToken(Guid userId, string email, string role)
|
// Phase 1: short-lived token with no program/role scope, only valid for SelectProgram
|
||||||
|
public string GenerateSessionToken(Guid userId, string email)
|
||||||
{
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Email, email),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||||
|
new Claim("user_id", userId.ToString()),
|
||||||
|
new Claim("auth_stage", "selecting_program")
|
||||||
|
};
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: _config["Jwt:Issuer"],
|
||||||
|
audience: null,
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddSeconds(_sessionTokenExpiryInSeconds),
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: full program-scoped token
|
||||||
|
public string GenerateToken(Guid userId, string email, string role, Guid programId)
|
||||||
|
{
|
||||||
if (UserRoles.TryParse(role) is null)
|
if (UserRoles.TryParse(role) is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Invalid role name");
|
throw new ArgumentException("Invalid role name");
|
||||||
@@ -29,14 +56,11 @@ public class TokenService
|
|||||||
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||||
new Claim(JwtRegisteredClaimNames.Email, email),
|
new Claim(JwtRegisteredClaimNames.Email, email),
|
||||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||||
new Claim("user_id", userId.ToString())
|
new Claim("user_id", userId.ToString()),
|
||||||
|
new Claim("program_id", programId.ToString()),
|
||||||
|
new Claim(ClaimTypes.Role, role)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (role is not null)
|
|
||||||
{
|
|
||||||
claims.Add(new Claim(ClaimTypes.Role, role));
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
|
||||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
DELIMITER ;;
|
||||||
|
CREATE DEFINER=`root`@`%` PROCEDURE `sp_UserPrograms_GetByUserId`(IN p_id_user CHAR(36))
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
up.id_program,
|
||||||
|
p.name AS program_name,
|
||||||
|
r.internal_name AS role_internal_name,
|
||||||
|
r.name AS role_display_name,
|
||||||
|
up.is_primary,
|
||||||
|
up.status
|
||||||
|
FROM user_program up
|
||||||
|
JOIN program p ON up.id_program = p.id_program
|
||||||
|
JOIN role r ON up.id_role = r.id_role
|
||||||
|
WHERE up.id_user = p_id_user
|
||||||
|
AND up.status = 'active'
|
||||||
|
ORDER BY up.is_primary DESC, p.name ASC;
|
||||||
|
END;;
|
||||||
|
DELIMITER ;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
DELIMITER ;;
|
||||||
|
CREATE DEFINER=`root`@`%` PROCEDURE `sp_User_GetById_WithProgram`(
|
||||||
|
IN p_id_user CHAR(36),
|
||||||
|
IN p_id_program CHAR(36)
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
u.id_user,
|
||||||
|
u.email,
|
||||||
|
u.name,
|
||||||
|
up.id_program,
|
||||||
|
p.name AS program_name,
|
||||||
|
r.internal_name AS role_internal_name,
|
||||||
|
r.name AS role_display_name,
|
||||||
|
up.status
|
||||||
|
FROM `user` u
|
||||||
|
JOIN user_program up ON u.id_user = up.id_user AND up.id_program = p_id_program
|
||||||
|
JOIN role r ON up.id_role = r.id_role
|
||||||
|
JOIN program p ON up.id_program = p.id_program
|
||||||
|
WHERE u.id_user = p_id_user
|
||||||
|
LIMIT 1;
|
||||||
|
END;;
|
||||||
|
DELIMITER ;
|
||||||
Reference in New Issue
Block a user