@@ -45,6 +45,8 @@ export class LoginComponent {
4545 }
4646
4747 submit ( ) : void {
48+ if ( this . loading ( ) ) return ; // ⬅️ guard against double/rapid submits
49+
4850 this . errorMessage . set ( null ) ;
4951
5052 if ( this . form . invalid ) {
@@ -70,7 +72,6 @@ export class LoginComponent {
7072 return ;
7173 }
7274
73- // Forced first-login password reset takes priority over everything.
7475 if ( data . must_change_password ) {
7576 this . router . navigate ( [ '/change-password' ] ) ;
7677 return ;
@@ -81,10 +82,11 @@ export class LoginComponent {
8182 this . auth . logout ( ) ;
8283 }
8384 } ,
84- // Login 401 is handled HERE locally (not via the global "Session expired" toast).
8585 error : ( err : HttpErrorResponse ) => {
8686 this . loading . set ( false ) ;
87- if ( err . status === 401 ) {
87+ if ( err . status === 429 ) {
88+ this . errorMessage . set ( this . rateLimitMessage ( err ) ) ;
89+ } else if ( err . status === 401 ) {
8890 this . errorMessage . set ( 'Invalid email or password.' ) ;
8991 } else if ( err . status === 0 ) {
9092 this . errorMessage . set ( 'Cannot reach the server. Check your connection and try again.' ) ;
@@ -118,13 +120,28 @@ export class LoginComponent {
118120 this . auth . logout ( ) ;
119121 }
120122 } ,
121- error : ( ) => {
123+ error : ( err : HttpErrorResponse ) => {
122124 this . mfaLoading . set ( false ) ;
123- this . mfaError . set ( 'Invalid or expired code. Try again or use a recovery code.' ) ;
125+ if ( err . status === 429 ) {
126+ this . mfaError . set ( this . rateLimitMessage ( err ) ) ;
127+ } else {
128+ this . mfaError . set ( 'Invalid or expired code. Try again or use a recovery code.' ) ;
129+ }
124130 } ,
125131 } ) ;
126132 }
127133
134+ /** Builds a friendly rate-limit message, using Retry-After header if the server sends one. */
135+ private rateLimitMessage ( err : HttpErrorResponse ) : string {
136+ const retryAfter = err . headers ?. get ?.( 'Retry-After' ) ;
137+ const seconds = retryAfter ? parseInt ( retryAfter , 10 ) : null ;
138+ if ( seconds && ! isNaN ( seconds ) && seconds > 0 ) {
139+ return `Too many attempts. Please wait ${ seconds } s and try again.` ;
140+ }
141+ return 'Too many attempts. Please wait a moment and try again.' ;
142+ }
143+
144+
128145 backToCredentials ( ) : void {
129146 this . step . set ( 'credentials' ) ;
130147 this . challengeToken . set ( null ) ;
0 commit comments