Add one-time password recovery flow
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user