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,
);
if (!verification || verification.code !== dto.smsCode) {
throw new BadRequestException('验证码错误或已过期');
if (!verification) {
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 '../../../core/router/routes.dart';
import '../../../core/constants/app_colors.dart';
import '../../../core/error/exceptions.dart';
import '../../providers/user_providers.dart';
class ForgotPasswordPage extends ConsumerStatefulWidget {
@ -22,6 +23,7 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
int _countDown = 0;
bool _sendingSms = false;
@override
void dispose() {
@ -41,21 +43,27 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
return;
}
setState(() => _sendingSms = true);
try {
await ref.read(userNotifierProvider.notifier).sendSmsCode(phone, 'RESET_PASSWORD');
if (mounted) {
setState(() => _countDown = 60);
_startCountDown();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('验证码已发送')),
);
}
} catch (e) {
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(
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) {
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(
SnackBar(content: Text('重置失败: $e')),
SnackBar(content: Text(msg)),
);
}
}
@ -191,7 +202,7 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
width: 120,
height: 56,
child: ElevatedButton(
onPressed: _countDown > 0 ? null : _sendSmsCode,
onPressed: (_countDown > 0 || _sendingSms) ? null : _sendSmsCode,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),