77using OpenShock . API . Errors ;
88using OpenShock . API . Models . Requests ;
99using OpenShock . API . Services . Turnstile ;
10+ using OpenShock . Common . Extensions ;
1011using OpenShock . Common . Problems ;
12+ using OpenShock . Common . Services . Bypass ;
1113using OpenShock . Common . Utils ;
1214
1315namespace OpenShock . API . Controller . Account ;
@@ -24,8 +26,8 @@ public sealed partial class AccountController
2426 [ ProducesResponseType ( StatusCodes . Status200OK ) ]
2527 [ ProducesResponseType < OpenShockProblem > ( StatusCodes . Status403Forbidden , MediaTypeNames . Application . ProblemJson ) ]
2628 [ MapToApiVersion ( "2" ) ]
27- public Task < IActionResult > PasswordResetInitiateV2 ( [ FromBody ] PasswordResetRequestV2 body , [ FromServices ] ICloudflareTurnstileService turnstileService , CancellationToken cancellationToken )
28- => PasswordResetInitiate ( body , turnstileService , cancellationToken ) ;
29+ public Task < IActionResult > PasswordResetInitiateV2 ( [ FromBody ] PasswordResetRequestV2 body , [ FromServices ] ICloudflareTurnstileService turnstileService , [ FromServices ] IBypassTokenService bypassTokens , CancellationToken cancellationToken )
30+ => PasswordResetInitiate ( body , turnstileService , bypassTokens , cancellationToken ) ;
2931
3032 /// <summary>
3133 /// Initiate a password reset. Deprecated: use POST /password-reset instead.
@@ -38,10 +40,10 @@ public Task<IActionResult> PasswordResetInitiateV2([FromBody] PasswordResetReque
3840 [ ProducesResponseType ( StatusCodes . Status200OK ) ]
3941 [ ProducesResponseType < OpenShockProblem > ( StatusCodes . Status403Forbidden , MediaTypeNames . Application . ProblemJson ) ]
4042 [ MapToApiVersion ( "2" ) ]
41- public Task < IActionResult > PasswordResetInitiateV2Legacy ( [ FromBody ] PasswordResetRequestV2 body , [ FromServices ] ICloudflareTurnstileService turnstileService , CancellationToken cancellationToken )
42- => PasswordResetInitiate ( body , turnstileService , cancellationToken ) ;
43+ public Task < IActionResult > PasswordResetInitiateV2Legacy ( [ FromBody ] PasswordResetRequestV2 body , [ FromServices ] ICloudflareTurnstileService turnstileService , [ FromServices ] IBypassTokenService bypassTokens , CancellationToken cancellationToken )
44+ => PasswordResetInitiate ( body , turnstileService , bypassTokens , cancellationToken ) ;
4345
44- private async Task < IActionResult > PasswordResetInitiate ( PasswordResetRequestV2 body , ICloudflareTurnstileService turnstileService , CancellationToken cancellationToken )
46+ private async Task < IActionResult > PasswordResetInitiate ( PasswordResetRequestV2 body , ICloudflareTurnstileService turnstileService , IBypassTokenService bypassTokens , CancellationToken cancellationToken )
4547 {
4648 var turnStile = await turnstileService . VerifyUserResponseTokenAsync ( body . TurnstileResponse , HttpContext . GetRemoteIP ( ) , cancellationToken ) ;
4749 if ( ! turnStile . IsT0 )
@@ -53,6 +55,11 @@ private async Task<IActionResult> PasswordResetInitiate(PasswordResetRequestV2 b
5355 return Problem ( new OpenShockProblem ( "InternalServerError" , "Internal Server Error" , HttpStatusCode . InternalServerError ) ) ;
5456 }
5557
58+ // If a bypass token resolved against an admin email, abort silently — same response shape as
59+ // a missing/non-admin email so the bypass scheme can't be used to enumerate admin addresses.
60+ if ( ! await bypassTokens . TryRecordUseByEmailAsync ( body . Email , cancellationToken ) )
61+ return Ok ( ) ;
62+
5663 await _accountService . CreatePasswordResetFlowAsync ( body . Email ) ;
5764
5865 return Ok ( ) ;
0 commit comments