feat: 实现密码重置功能与用户搜索API,优化注册登录流程
- 新增忘记密码页面与重置密码确认流程(前端+后端) - 修复注册验证码页登录跳转路由 - 新增用户搜索API(按邮箱查询) - 简化infra脚本,统一为app.sh - 补充密码重置与用户API测试覆盖 - 更新runtime文档与AGENTS配置
This commit is contained in:
@@ -12,6 +12,7 @@ class RegisterState extends Equatable {
|
||||
final Email email;
|
||||
final Password password;
|
||||
final VerificationCode verificationCode;
|
||||
final String inviteCode;
|
||||
final FormzSubmissionStatus status;
|
||||
final String? errorMessage;
|
||||
final String? pendingEmail;
|
||||
@@ -23,6 +24,7 @@ class RegisterState extends Equatable {
|
||||
this.email = const Email.pure(),
|
||||
this.password = const Password.pure(),
|
||||
this.verificationCode = const VerificationCode.pure(),
|
||||
this.inviteCode = '',
|
||||
this.status = FormzSubmissionStatus.initial,
|
||||
this.errorMessage,
|
||||
this.pendingEmail,
|
||||
@@ -39,6 +41,7 @@ class RegisterState extends Equatable {
|
||||
Email? email,
|
||||
Password? password,
|
||||
VerificationCode? verificationCode,
|
||||
String? inviteCode,
|
||||
FormzSubmissionStatus? status,
|
||||
String? errorMessage,
|
||||
String? pendingEmail,
|
||||
@@ -50,6 +53,7 @@ class RegisterState extends Equatable {
|
||||
email: email ?? this.email,
|
||||
password: password ?? this.password,
|
||||
verificationCode: verificationCode ?? this.verificationCode,
|
||||
inviteCode: inviteCode ?? this.inviteCode,
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage,
|
||||
pendingEmail: pendingEmail ?? this.pendingEmail,
|
||||
@@ -64,6 +68,7 @@ class RegisterState extends Equatable {
|
||||
email,
|
||||
password,
|
||||
verificationCode,
|
||||
inviteCode,
|
||||
status,
|
||||
errorMessage,
|
||||
pendingEmail,
|
||||
@@ -93,6 +98,10 @@ class RegisterCubit extends Cubit<RegisterState> {
|
||||
emit(state.copyWith(verificationCode: VerificationCode.dirty(value)));
|
||||
}
|
||||
|
||||
void inviteCodeChanged(String value) {
|
||||
emit(state.copyWith(inviteCode: value));
|
||||
}
|
||||
|
||||
Future<bool> submitStep1() async {
|
||||
if (!state.isStep1Valid) return false;
|
||||
|
||||
@@ -104,6 +113,7 @@ class RegisterCubit extends Cubit<RegisterState> {
|
||||
username: state.username.value,
|
||||
email: state.email.value,
|
||||
password: state.password.value,
|
||||
inviteCode: state.inviteCode.isNotEmpty ? state.inviteCode : null,
|
||||
),
|
||||
);
|
||||
emit(
|
||||
@@ -202,6 +212,7 @@ class RegisterCubit extends Cubit<RegisterState> {
|
||||
username: state.username.value,
|
||||
email: state.email.value,
|
||||
password: state.password.value,
|
||||
inviteCode: state.inviteCode.isNotEmpty ? state.inviteCode : null,
|
||||
),
|
||||
);
|
||||
emit(
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:formz/formz.dart';
|
||||
import '../../../../core/form_inputs/form_inputs.dart';
|
||||
import '../../data/auth_repository.dart';
|
||||
|
||||
class ResetPasswordState extends Equatable {
|
||||
final Email email;
|
||||
final VerificationCode code;
|
||||
final Password newPassword;
|
||||
final Password confirmPassword;
|
||||
final FormzSubmissionStatus status;
|
||||
final String? errorMessage;
|
||||
final bool isSuccess;
|
||||
final int resendCountdown;
|
||||
final bool codeSent;
|
||||
|
||||
const ResetPasswordState({
|
||||
this.email = const Email.pure(),
|
||||
this.code = const VerificationCode.pure(),
|
||||
this.newPassword = const Password.pure(),
|
||||
this.confirmPassword = const Password.pure(),
|
||||
this.status = FormzSubmissionStatus.initial,
|
||||
this.errorMessage,
|
||||
this.isSuccess = false,
|
||||
this.resendCountdown = 0,
|
||||
this.codeSent = false,
|
||||
});
|
||||
|
||||
bool get canSubmit {
|
||||
if (!codeSent) {
|
||||
return email.isValid && status != FormzSubmissionStatus.inProgress;
|
||||
}
|
||||
return email.isValid &&
|
||||
code.isValid &&
|
||||
newPassword.isValid &&
|
||||
confirmPassword.isValid &&
|
||||
newPassword.value == confirmPassword.value &&
|
||||
status != FormzSubmissionStatus.inProgress;
|
||||
}
|
||||
|
||||
ResetPasswordState copyWith({
|
||||
Email? email,
|
||||
VerificationCode? code,
|
||||
Password? newPassword,
|
||||
Password? confirmPassword,
|
||||
FormzSubmissionStatus? status,
|
||||
String? errorMessage,
|
||||
bool? isSuccess,
|
||||
int? resendCountdown,
|
||||
bool? codeSent,
|
||||
}) {
|
||||
return ResetPasswordState(
|
||||
email: email ?? this.email,
|
||||
code: code ?? this.code,
|
||||
newPassword: newPassword ?? this.newPassword,
|
||||
confirmPassword: confirmPassword ?? this.confirmPassword,
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage,
|
||||
isSuccess: isSuccess ?? this.isSuccess,
|
||||
resendCountdown: resendCountdown ?? this.resendCountdown,
|
||||
codeSent: codeSent ?? this.codeSent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
email,
|
||||
code,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
status,
|
||||
errorMessage,
|
||||
isSuccess,
|
||||
resendCountdown,
|
||||
codeSent,
|
||||
];
|
||||
}
|
||||
|
||||
class ResetPasswordCubit extends Cubit<ResetPasswordState> {
|
||||
final AuthRepository _repository;
|
||||
Timer? _resendTimer;
|
||||
|
||||
ResetPasswordCubit(this._repository) : super(const ResetPasswordState());
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_resendTimer?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void emailChanged(String value) {
|
||||
emit(state.copyWith(email: Email.dirty(value), errorMessage: null));
|
||||
}
|
||||
|
||||
void codeChanged(String value) {
|
||||
emit(
|
||||
state.copyWith(code: VerificationCode.dirty(value), errorMessage: null),
|
||||
);
|
||||
}
|
||||
|
||||
void newPasswordChanged(String value) {
|
||||
emit(
|
||||
state.copyWith(newPassword: Password.dirty(value), errorMessage: null),
|
||||
);
|
||||
}
|
||||
|
||||
void confirmPasswordChanged(String value) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
confirmPassword: Password.dirty(value),
|
||||
errorMessage: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> sendCode() async {
|
||||
if (state.status == FormzSubmissionStatus.inProgress ||
|
||||
state.resendCountdown > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.email.isValid) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: state.email.value.isEmpty ? '请输入邮箱' : '邮箱格式不正确',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.inProgress,
|
||||
codeSent: true,
|
||||
resendCountdown: 60,
|
||||
errorMessage: null,
|
||||
),
|
||||
);
|
||||
_startResendCountdown();
|
||||
|
||||
try {
|
||||
await _repository.requestPasswordReset(state.email.value);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.success,
|
||||
errorMessage: 'CODE_SENT_SUCCESS',
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
_cancelResendCountdown();
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
codeSent: false,
|
||||
resendCountdown: 0,
|
||||
errorMessage: '网络错误,请稍后重试',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _cancelResendCountdown() {
|
||||
_resendTimer?.cancel();
|
||||
}
|
||||
|
||||
void _startResendCountdown() {
|
||||
_cancelResendCountdown();
|
||||
_resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
final newCountdown = state.resendCountdown - 1;
|
||||
if (newCountdown <= 0) {
|
||||
timer.cancel();
|
||||
emit(state.copyWith(resendCountdown: 0));
|
||||
} else {
|
||||
emit(state.copyWith(resendCountdown: newCountdown));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> resendCode() async {
|
||||
if (state.resendCountdown > 0 ||
|
||||
state.status == FormzSubmissionStatus.inProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.email.isValid) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: state.email.value.isEmpty ? '请输入邮箱' : '邮箱格式不正确',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.inProgress,
|
||||
codeSent: true,
|
||||
resendCountdown: 60,
|
||||
errorMessage: null,
|
||||
),
|
||||
);
|
||||
_startResendCountdown();
|
||||
|
||||
try {
|
||||
await _repository.requestPasswordReset(state.email.value);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.success,
|
||||
errorMessage: 'CODE_SENT_SUCCESS',
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
_cancelResendCountdown();
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
resendCountdown: 0,
|
||||
errorMessage: '网络错误,请稍后重试',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> submit() async {
|
||||
if (!state.codeSent) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: '请先获取验证码',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.email.isValid) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: '请输入有效的邮箱地址',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.code.isValid) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: '请输入6位验证码',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.newPassword.isValid) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: '新密码至少6位',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.confirmPassword.isValid) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: '请输入确认密码',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.newPassword.value != state.confirmPassword.value) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: '两次密码输入不一致',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.inProgress,
|
||||
errorMessage: null,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await _repository.confirmPasswordReset(
|
||||
email: state.email.value,
|
||||
token: state.code.value,
|
||||
newPassword: state.newPassword.value,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(status: FormzSubmissionStatus.success, isSuccess: true),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: FormzSubmissionStatus.failure,
|
||||
errorMessage: '密码重置失败,请检查验证码',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user