fix(auth): 修复找回密码流程的三个 bug

Backend:
- password.service.ts: resetPassword 补齐验证码暴力破解防护,
  输错时累计 attempts,超过 5 次拒绝,与 loginBySms 逻辑一致

Frontend (forgot_password_page.dart):
- 发送验证码错误消息改为提取真实 message,不再显示原始异常类名
- 重置密码错误消息同上处理
- 新增 _sendingSms 标志,发送请求期间禁用按钮,防止重复发短信

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-10 19:56:34 -07:00
parent 761dcb1115
commit 149cf7ea77
2 changed files with 25 additions and 7 deletions

View File

@ -45,8 +45,15 @@ export class PasswordService {
SmsVerificationType.RESET_PASSWORD, SmsVerificationType.RESET_PASSWORD,
); );
if (!verification || verification.code !== dto.smsCode) { if (!verification) {
throw new BadRequestException('验证码错误或已过期'); throw new BadRequestException('验证码已过期或不存在');
}
if (verification.attempts >= 5) {
throw new BadRequestException('验证码尝试次数过多,请重新获取');
}
if (verification.code !== dto.smsCode) {
await this.smsVerificationRepository.incrementAttempts(verification.id);
throw new BadRequestException('验证码错误');
} }
// 标记验证码已使用 // 标记验证码已使用

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../core/router/routes.dart'; import '../../../core/router/routes.dart';
import '../../../core/constants/app_colors.dart'; import '../../../core/constants/app_colors.dart';
import '../../../core/error/exceptions.dart';
import '../../providers/user_providers.dart'; import '../../providers/user_providers.dart';
class ForgotPasswordPage extends ConsumerStatefulWidget { class ForgotPasswordPage extends ConsumerStatefulWidget {
@ -22,6 +23,7 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
bool _obscurePassword = true; bool _obscurePassword = true;
bool _obscureConfirmPassword = true; bool _obscureConfirmPassword = true;
int _countDown = 0; int _countDown = 0;
bool _sendingSms = false;
@override @override
void dispose() { void dispose() {
@ -41,21 +43,27 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
return; return;
} }
setState(() => _sendingSms = true);
try { try {
await ref.read(userNotifierProvider.notifier).sendSmsCode(phone, 'RESET_PASSWORD'); await ref.read(userNotifierProvider.notifier).sendSmsCode(phone, 'RESET_PASSWORD');
setState(() => _countDown = 60);
_startCountDown();
if (mounted) { if (mounted) {
setState(() => _countDown = 60);
_startCountDown();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('验证码已发送')), const SnackBar(content: Text('验证码已发送')),
); );
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
final msg = e is ServerException || e is NetworkException
? (e as dynamic).message as String
: e.toString().replaceFirst(RegExp(r'^[A-Za-z]+Exception:\s*'), '');
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('发送失败: $e')), SnackBar(content: Text(msg)),
); );
} }
} finally {
if (mounted) setState(() => _sendingSms = false);
} }
} }
@ -86,8 +94,11 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
final msg = e is ServerException || e is NetworkException
? (e as dynamic).message as String
: e.toString().replaceFirst(RegExp(r'^[A-Za-z]+Exception:\s*'), '');
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('重置失败: $e')), SnackBar(content: Text(msg)),
); );
} }
} }
@ -191,7 +202,7 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
width: 120, width: 120,
height: 56, height: 56,
child: ElevatedButton( child: ElevatedButton(
onPressed: _countDown > 0 ? null : _sendSmsCode, onPressed: (_countDown > 0 || _sendingSms) ? null : _sendSmsCode,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),