Add one-time password recovery flow

This commit is contained in:
Hamza Ayed
2026-10-03 13:57:12 +03:00
parent 9945e31e7c
commit ceae822302
9 changed files with 427 additions and 2 deletions
@@ -229,6 +229,27 @@ class ApiRepository {
required String password,
}) => _authenticateAccount('login', email, password);
Future<void> requestPasswordReset(String email) async {
final response = await http.post(
Uri.parse('$_baseUrl/v1/auth/password-reset/request'),
headers: const {'Content-Type': 'application/json'},
body: jsonEncode({'email': email}),
);
_checkStatus(response);
}
Future<void> completePasswordReset({
required String token,
required String newPassword,
}) async {
final response = await http.post(
Uri.parse('$_baseUrl/v1/auth/password-reset/complete'),
headers: const {'Content-Type': 'application/json'},
body: jsonEncode({'token': token, 'new_password': newPassword}),
);
_checkStatus(response);
}
Future<Map<String, dynamic>> _authenticateAccount(
String action,
String email,
@@ -578,6 +578,15 @@ class _ChatPageState extends State<ChatPage> {
: 'ليس لديك حساب؟ أنشئ حسابًا',
),
),
if (!register)
TextButton(
onPressed: () {
final email = emailController.text.trim();
Navigator.pop(dialogContext);
_showPasswordResetFlow(api, email);
},
child: const Text('نسيت كلمة المرور؟'),
),
],
),
),
@@ -654,6 +663,143 @@ class _ChatPageState extends State<ChatPage> {
}
}
Future<void> _showPasswordResetFlow(
ApiRepository api,
String initialEmail,
) async {
final emailController = TextEditingController(text: initialEmail);
final email = await showDialog<String>(
context: context,
builder:
(dialogContext) => Directionality(
textDirection: TextDirection.rtl,
child: AlertDialog(
title: const Text('استعادة كلمة المرور'),
content: TextField(
controller: emailController,
autofocus: true,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'البريد الإلكتروني',
border: OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء'),
),
FilledButton(
onPressed: () => Navigator.pop(
dialogContext,
emailController.text.trim(),
),
child: const Text('إرسال الرمز'),
),
],
),
),
);
emailController.dispose();
if (email == null || email.isEmpty || !mounted) return;
try {
await api.requestPasswordReset(email);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('إذا كان البريد مرتبطًا بحساب، فستصلك رسالة استعادة.'),
),
);
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error.toString().replaceFirst('Exception: ', '')),
),
);
return;
}
final tokenController = TextEditingController();
final passwordController = TextEditingController();
final reset = await showDialog<(String, String)?>(
context: context,
builder:
(dialogContext) => Directionality(
textDirection: TextDirection.rtl,
child: AlertDialog(
title: const Text('أدخل رمز الاستعادة'),
content: SizedBox(
width: 360,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: tokenController,
decoration: const InputDecoration(
labelText: 'الرمز المرسل إلى بريدك',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'كلمة المرور الجديدة (12 محرفًا على الأقل)',
border: OutlineInputBorder(),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء'),
),
FilledButton(
onPressed: () {
if (tokenController.text.trim().length < 32 ||
passwordController.text.length < 12) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
const SnackBar(
content: Text('أدخل رمزًا صالحًا وكلمة مرور من 12 محرفًا على الأقل.'),
),
);
return;
}
Navigator.pop(dialogContext, (
tokenController.text.trim(),
passwordController.text,
));
},
child: const Text('تغيير كلمة المرور'),
),
],
),
),
);
tokenController.dispose();
passwordController.dispose();
if (reset == null || !mounted) return;
try {
await api.completePasswordReset(token: reset.$1, newPassword: reset.$2);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('تم تغيير كلمة المرور. سجّل الدخول بها الآن.')),
);
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error.toString().replaceFirst('Exception: ', '')),
),
);
}
}
Future<void> _toggleRecording(VoiceState voice) async {
final cubit = context.read<VoiceCubit>();
if (voice.status == VoiceStatus.recording) {