1- using System . Security . Claims ;
1+ using System . ComponentModel . DataAnnotations ;
2+ using System . Security . Claims ;
23using LogMkApi . Data . Models ;
34using LogMkApi . Services ;
45using Microsoft . AspNetCore . Authorization ;
56using Microsoft . AspNetCore . Mvc ;
7+ using Microsoft . AspNetCore . RateLimiting ;
68using ServiceStack . Data ;
79using ServiceStack . OrmLite ;
810using ServiceStack . OrmLite . Dapper ;
@@ -16,29 +18,53 @@ public class AuthController : Controller
1618 private readonly IDbConnectionFactory _dbFactory ;
1719 private readonly AuthService _authService ;
1820 private readonly PasswordService _passwordService ;
21+ private readonly RefreshTokenService _refreshTokenService ;
22+ private readonly IConfiguration _configuration ;
1923
20-
21- public AuthController ( IDbConnectionFactory dbFactory , AuthService authService , PasswordService passwordService )
24+ public AuthController ( IDbConnectionFactory dbFactory , AuthService authService , PasswordService passwordService , RefreshTokenService refreshTokenService , IConfiguration configuration )
2225 {
2326 _dbFactory = dbFactory ;
2427 _authService = authService ;
2528 _passwordService = passwordService ;
29+ _refreshTokenService = refreshTokenService ;
30+ _configuration = configuration ;
2631 }
2732 [ AllowAnonymous ]
33+ [ EnableRateLimiting ( "AuthPolicy" ) ]
2834 [ HttpPost ( "login" ) ]
2935 public async Task < IActionResult > Login ( [ FromBody ] LoginRequest request )
3036 {
31- using ( var db = _dbFactory . OpenDbConnection ( ) )
37+ if ( ! ModelState . IsValid )
38+ {
39+ return BadRequest ( ModelState ) ;
40+ }
41+
42+ try
3243 {
33- // In a real-world scenario, you would retrieve the user from a database
34- var user = ( await db . QueryAsync < User > ( "SELECT * FROM User WHERE UserName = @UserName" , new { UserName = request . UserName } ) ) . FirstOrDefault ( ) ;
35- if ( user == null || ! _authService . ValidateUser ( user , request . Password ) )
44+ using ( var db = _dbFactory . OpenDbConnection ( ) )
3645 {
37- return Unauthorized ( "Invalid email or password." ) ;
38- }
46+ // In a real-world scenario, you would retrieve the user from a database
47+ var user = ( await db . QueryAsync < User > ( "SELECT * FROM User WHERE UserName = @UserName" , new { UserName = request . UserName } ) ) . FirstOrDefault ( ) ;
48+ if ( user == null || ! _authService . ValidateUser ( user , request . Password ) )
49+ {
50+ return Unauthorized ( new { Error = "Invalid username or password" } ) ;
51+ }
52+
53+ var token = _authService . GenerateJwtToken ( user ) ;
54+ var refreshToken = await _refreshTokenService . CreateRefreshTokenAsync ( user . Id ) ;
3955
40- var token = _authService . GenerateJwtToken ( user ) ;
41- return Ok ( new { Token = token } ) ;
56+ return Ok ( new LoginResponse
57+ {
58+ AccessToken = token ,
59+ RefreshToken = refreshToken . Token ,
60+ ExpiresIn = _configuration . GetValue < int > ( "JwtSettings:ExpiryMinutes" , 30 ) * 60
61+ } ) ;
62+ }
63+ }
64+ catch ( Exception ex )
65+ {
66+ // Log the exception but don't expose internal details
67+ return StatusCode ( 500 , new { Error = "An error occurred during authentication" } ) ;
4268 }
4369 }
4470 [ Authorize ]
@@ -63,44 +89,153 @@ public async Task<IActionResult> GetUser()
6389 }
6490 }
6591 [ Authorize ]
92+ [ EnableRateLimiting ( "AuthPolicy" ) ]
6693 [ HttpPost ( "change-password" ) ]
6794 public async Task < IActionResult > ChangePasswordAsync ( [ FromBody ] ChangePasswordRequest request )
6895 {
96+ if ( ! ModelState . IsValid )
97+ {
98+ return BadRequest ( ModelState ) ;
99+ }
100+
69101 var userName = User . Claims . FirstOrDefault ( c => c . Type == ClaimTypes . NameIdentifier ) ? . Value ;
70102 if ( userName == null )
71103 {
72- return Unauthorized ( ) ;
104+ return Unauthorized ( new { Error = "User not authenticated" } ) ;
73105 }
74- using ( var db = _dbFactory . OpenDbConnection ( ) )
106+
107+ try
75108 {
76- var user = ( await db . QueryAsync < User > ( "SELECT * FROM User WHERE UserName = @UserName" , new { UserName = userName } ) ) . FirstOrDefault ( ) ;
77- if ( user == null )
78- {
79- return NotFound ( ) ;
80- }
81- if ( ! _authService . ValidateUser ( user , request . OldPassword ) )
109+ using ( var db = _dbFactory . OpenDbConnection ( ) )
82110 {
83- return Unauthorized ( "Invalid password." ) ;
111+ var user = ( await db . QueryAsync < User > ( "SELECT * FROM User WHERE UserName = @UserName" , new { UserName = userName } ) ) . FirstOrDefault ( ) ;
112+ if ( user == null )
113+ {
114+ return NotFound ( new { Error = "User not found" } ) ;
115+ }
116+ if ( ! _authService . ValidateUser ( user , request . OldPassword ) )
117+ {
118+ return Unauthorized ( new { Error = "Current password is incorrect" } ) ;
119+ }
120+ user . PasswordHash = _passwordService . HashPassword ( user , request . NewPassword ) ;
121+ await db . UpdateAsync ( user ) ;
122+ return Ok ( new { Message = "Password changed successfully" } ) ;
84123 }
85- user . PasswordHash = _passwordService . HashPassword ( user , request . NewPassword ) ;
86- await db . UpdateAsync ( user ) ;
87- return Ok ( ) ;
88124 }
125+ catch ( Exception ex )
126+ {
127+ // Log the exception but don't expose internal details
128+ return StatusCode ( 500 , new { Error = "An error occurred while changing the password" } ) ;
129+ }
130+ }
131+
132+ [ AllowAnonymous ]
133+ [ HttpPost ( "refresh" ) ]
134+ public async Task < IActionResult > RefreshToken ( [ FromBody ] RefreshTokenRequest request )
135+ {
136+ var principal = _authService . GetPrincipalFromExpiredToken ( request . AccessToken ) ;
137+ if ( principal == null )
138+ {
139+ return BadRequest ( new { Error = "Invalid access token" } ) ;
140+ }
141+
142+ var userId = _authService . GetUserIdFromPrincipal ( principal ) ;
143+ if ( ! userId . HasValue )
144+ {
145+ return BadRequest ( new { Error = "Invalid access token" } ) ;
146+ }
147+
148+ if ( ! await _refreshTokenService . ValidateRefreshTokenAsync ( request . RefreshToken ) )
149+ {
150+ return Unauthorized ( new { Error = "Invalid refresh token" } ) ;
151+ }
152+
153+ using var db = _dbFactory . OpenDbConnection ( ) ;
154+ var user = ( await db . SelectAsync < User > ( u => u . Id == userId . Value ) ) . FirstOrDefault ( ) ;
155+ if ( user == null )
156+ {
157+ return NotFound ( new { Error = "User not found" } ) ;
158+ }
159+
160+ // Rotate refresh token
161+ var newRefreshToken = await _refreshTokenService . RotateRefreshTokenAsync ( request . RefreshToken , user . Id ) ;
162+ var newAccessToken = _authService . GenerateJwtToken ( user ) ;
163+
164+ return Ok ( new LoginResponse
165+ {
166+ AccessToken = newAccessToken ,
167+ RefreshToken = newRefreshToken . Token ,
168+ ExpiresIn = _configuration . GetValue < int > ( "JwtSettings:ExpiryMinutes" , 30 ) * 60
169+ } ) ;
170+ }
171+
172+ [ HttpPost ( "revoke" ) ]
173+ public async Task < IActionResult > RevokeToken ( [ FromBody ] RevokeTokenRequest request )
174+ {
175+ await _refreshTokenService . RevokeRefreshTokenAsync ( request . RefreshToken ) ;
176+ return Ok ( new { Message = "Token revoked successfully" } ) ;
177+ }
178+
179+ [ HttpPost ( "logout" ) ]
180+ public async Task < IActionResult > Logout ( )
181+ {
182+ var userId = _authService . GetUserIdFromPrincipal ( User ) ;
183+ if ( userId . HasValue )
184+ {
185+ await _refreshTokenService . RevokeAllUserRefreshTokensAsync ( userId . Value ) ;
186+ }
187+ return Ok ( new { Message = "Logged out successfully" } ) ;
89188 }
90189
91190}
92191public class ChangePasswordRequest
93192{
193+ [ Required ( ErrorMessage = "Old password is required" ) ]
194+ [ StringLength ( 256 , MinimumLength = 1 , ErrorMessage = "Old password must be between 1 and 256 characters" ) ]
94195 public required string OldPassword { get ; set ; }
196+
197+ [ Required ( ErrorMessage = "New password is required" ) ]
198+ [ StringLength ( 256 , MinimumLength = 8 , ErrorMessage = "New password must be between 8 and 256 characters" ) ]
199+ [ RegularExpression ( @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]" ,
200+ ErrorMessage = "New password must contain at least one lowercase letter, one uppercase letter, one digit, and one special character" ) ]
95201 public required string NewPassword { get ; set ; }
96202}
203+
97204public class LoginRequest
98205{
206+ [ Required ( ErrorMessage = "Username is required" ) ]
207+ [ StringLength ( 100 , MinimumLength = 3 , ErrorMessage = "Username must be between 3 and 100 characters" ) ]
208+ [ RegularExpression ( @"^[a-zA-Z0-9_@.-]+$" , ErrorMessage = "Username can only contain letters, numbers, underscores, periods, hyphens, and @ symbols" ) ]
99209 public required string UserName { get ; set ; }
210+
211+ [ Required ( ErrorMessage = "Password is required" ) ]
212+ [ StringLength ( 256 , MinimumLength = 1 , ErrorMessage = "Password must be between 1 and 256 characters" ) ]
100213 public required string Password { get ; set ; }
101214}
102215public class UserResponse
103216{
104217 public Guid Id { get ; set ; }
105218 public required string UserName { get ; set ; }
106219}
220+
221+ public class LoginResponse
222+ {
223+ public required string AccessToken { get ; set ; }
224+ public required string RefreshToken { get ; set ; }
225+ public int ExpiresIn { get ; set ; }
226+ }
227+
228+ public class RefreshTokenRequest
229+ {
230+ [ Required ( ErrorMessage = "Access token is required" ) ]
231+ public required string AccessToken { get ; set ; }
232+
233+ [ Required ( ErrorMessage = "Refresh token is required" ) ]
234+ public required string RefreshToken { get ; set ; }
235+ }
236+
237+ public class RevokeTokenRequest
238+ {
239+ [ Required ( ErrorMessage = "Refresh token is required" ) ]
240+ public required string RefreshToken { get ; set ; }
241+ }
0 commit comments