From 75451ebc3f339d539693485ec57ec63634361d05 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Tue, 8 Sep 2026 17:51:07 +0300 Subject: [PATCH] Update Saqel Platform: 2026-09-08 17:51:07 --- .../lib/core/services/storage_service.dart | 7 ++++- .../data/repositories/app_repositories.dart | 2 +- .../lib/logic/cubits/auth_cubit.dart | 2 +- .../screens/auth/auth_screen.dart | 28 +++++++++++++++++-- .../lib/core/services/storage_service.dart | 6 +++- .../lib/data/models/teacher_models.dart | 16 +++++------ .../data/repositories/teacher_repository.dart | 4 +-- .../lib/logic/cubits/teacher_auth_cubit.dart | 12 ++++---- .../screens/auth/teacher_auth_screen.dart | 17 +++++------ .../screens/teacher_main_shell.dart | 14 +++++----- apps/teacher_app/macos/Podfile.lock | 6 ++++ backend/app/Controllers/AuthController.php | 9 ++++-- 12 files changed, 84 insertions(+), 39 deletions(-) diff --git a/apps/student_app/lib/core/services/storage_service.dart b/apps/student_app/lib/core/services/storage_service.dart index 251370a..bfec8e9 100644 --- a/apps/student_app/lib/core/services/storage_service.dart +++ b/apps/student_app/lib/core/services/storage_service.dart @@ -6,6 +6,9 @@ import '../utils/app_logger.dart'; /// Enterprise Storage Service with Device-Bound Payload Obfuscation & Secure KeyStore/Keychain class StorageService { + // Keeps a newly issued session available during this app run if the macOS + // debug runner has no Keychain entitlement. Production still uses Keychain. + static final Map _volatileStore = {}; final FlutterSecureStorage _secureStorage; StorageService({FlutterSecureStorage? secureStorage}) @@ -26,6 +29,7 @@ class StorageService { static const String _keyDeviceId = 'saqel_device_id'; Future _writeSafe(String key, String value) async { + _volatileStore[key] = value; try { await _secureStorage.write(key: key, value: value); } on PlatformException catch (e) { @@ -38,10 +42,11 @@ class StorageService { final val = await _secureStorage.read(key: key); if (val != null && val.isNotEmpty) return val; } catch (_) {} - return null; + return _volatileStore[key]; } Future _deleteSafe(String key) async { + _volatileStore.remove(key); try { await _secureStorage.delete(key: key); } catch (_) {} diff --git a/apps/student_app/lib/data/repositories/app_repositories.dart b/apps/student_app/lib/data/repositories/app_repositories.dart index 8e3bc1b..47391ca 100644 --- a/apps/student_app/lib/data/repositories/app_repositories.dart +++ b/apps/student_app/lib/data/repositories/app_repositories.dart @@ -88,7 +88,7 @@ class AuthRepository { required String identityToken, required String fullName, required String nationalId, - String gradeLevel = 'tawjihi_2008', + String gradeLevel = 'grade_10', String stream = 'scientific', }) async { final res = await _api.post( diff --git a/apps/student_app/lib/logic/cubits/auth_cubit.dart b/apps/student_app/lib/logic/cubits/auth_cubit.dart index b5feca6..ae7969e 100644 --- a/apps/student_app/lib/logic/cubits/auth_cubit.dart +++ b/apps/student_app/lib/logic/cubits/auth_cubit.dart @@ -148,7 +148,7 @@ class AuthCubit extends Cubit { required String identityToken, required String fullName, required String nationalId, - String gradeLevel = 'tawjihi_2008', + String gradeLevel = 'grade_10', String stream = 'scientific', }) async { AppLogger.log('Setting up profile for $fullName ($nationalId)...', tag: 'AUTH_CUBIT'); diff --git a/apps/student_app/lib/presentation/screens/auth/auth_screen.dart b/apps/student_app/lib/presentation/screens/auth/auth_screen.dart index 0742118..b106c0f 100644 --- a/apps/student_app/lib/presentation/screens/auth/auth_screen.dart +++ b/apps/student_app/lib/presentation/screens/auth/auth_screen.dart @@ -23,7 +23,7 @@ class _AuthScreenState extends State { final TextEditingController _fullNameController = TextEditingController(); String _selectedRole = 'student'; - final String _selectedGrade = 'tawjihi_2008'; + String _selectedGrade = 'grade_10'; String _selectedStream = 'scientific'; @override @@ -440,7 +440,30 @@ class _AuthScreenState extends State { ), const SizedBox(height: 18), - // Stream Selector (العلمي / الأدبي) + Text( + 'الصف الدراسي', + style: AppTypography.titleMedium.copyWith(color: AppColors.textSecondaryDark, fontSize: 13), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + ('grade_10', 'الصف العاشر'), + ('grade_11', 'الأول ثانوي'), + ('grade_12', 'الثاني ثانوي'), + ].map((grade) => ChoiceChip( + label: Text(grade.$2), + selected: _selectedGrade == grade.$1, + onSelected: isLoading ? null : (_) => setState(() { + _selectedGrade = grade.$1; + if (_selectedGrade == 'grade_10') _selectedStream = 'general'; + }), + )).toList(), + ), + const SizedBox(height: 18), + + // Stream Selector (only secondary grades have a stream) + if (_selectedGrade != 'grade_10') ...[ Text( 'الفرع الأكاديمي', style: AppTypography.titleMedium.copyWith(color: AppColors.textSecondaryDark, fontSize: 13), @@ -501,6 +524,7 @@ class _AuthScreenState extends State { ), ], ), + ], const SizedBox(height: 24), LuxuryButton( diff --git a/apps/teacher_app/lib/core/services/storage_service.dart b/apps/teacher_app/lib/core/services/storage_service.dart index cb2dc1f..3b61c6a 100644 --- a/apps/teacher_app/lib/core/services/storage_service.dart +++ b/apps/teacher_app/lib/core/services/storage_service.dart @@ -4,6 +4,7 @@ import '../utils/app_logger.dart'; /// Ultra-Resilient Storage Service for Teacher Studio class StorageService { + static final Map _volatileStore = {}; final FlutterSecureStorage _secureStorage; StorageService({FlutterSecureStorage? secureStorage}) @@ -21,6 +22,7 @@ class StorageService { static const String _keyUser = 'saqel_teacher_user_data'; Future _writeSafe(String key, String value) async { + _volatileStore[key] = value; try { await _secureStorage.write(key: key, value: value); } on PlatformException catch (e) { @@ -34,13 +36,15 @@ class StorageService { if (val != null && val.isNotEmpty) return val; } catch (_) {} - return null; + return _volatileStore[key]; } Future saveToken(String token) async => await _writeSafe(_keyToken, token); Future getToken() async => await _readSafe(_keyToken); Future clearSession() async { + _volatileStore.remove(_keyToken); + _volatileStore.remove(_keyUser); try { await _secureStorage.delete(key: _keyToken); await _secureStorage.delete(key: _keyUser); diff --git a/apps/teacher_app/lib/data/models/teacher_models.dart b/apps/teacher_app/lib/data/models/teacher_models.dart index 5cc0b4f..d1b4d30 100644 --- a/apps/teacher_app/lib/data/models/teacher_models.dart +++ b/apps/teacher_app/lib/data/models/teacher_models.dart @@ -184,20 +184,20 @@ class TeacherProfileModel { grades = [profile['grades_taught'].toString()]; } if (grades.isEmpty) { - grades = ['الصف العاشر الأساسي', 'الأول ثانوي العلمي']; + grades = []; } return TeacherProfileModel( id: user['id'] is int ? user['id'] : 1, uuid: user['uuid']?.toString() ?? '', - name: user['full_name']?.toString() ?? 'الأستاذ المعتمد', - specialization: profile['specialization']?.toString() ?? 'الفيزياء', - schoolName: profile['school_name']?.toString() ?? 'مدرسة الملك عبد الله الثاني للتميز', + name: user['full_name']?.toString() ?? 'حساب معلم غير مكتمل', + specialization: profile['specialization']?.toString() ?? 'بانتظار استكمال التخصص', + schoolName: profile['school_name']?.toString() ?? '', gradesTaught: grades, - bio: profile['bio']?.toString() ?? 'معلم معتمد في منصة صَقِل', - rating: 4.9, - studentsCount: 42, - lessonsCount: 28, + bio: profile['bio']?.toString() ?? '', + rating: 0, + studentsCount: 0, + lessonsCount: 0, ); } diff --git a/apps/teacher_app/lib/data/repositories/teacher_repository.dart b/apps/teacher_app/lib/data/repositories/teacher_repository.dart index 557fea1..b4ce7ff 100644 --- a/apps/teacher_app/lib/data/repositories/teacher_repository.dart +++ b/apps/teacher_app/lib/data/repositories/teacher_repository.dart @@ -56,7 +56,7 @@ class TeacherRepository { 'phone_number': phoneNumber, 'role': 'teacher', }), - ).timeout(const Duration(seconds: 6)); + ).timeout(const Duration(seconds: 30)); return json.decode(response.body); } catch (e) { @@ -78,7 +78,7 @@ class TeacherRepository { if (fullName != null) 'full_name': fullName, 'device_fingerprint': 'saqel_teacher_flutter', }), - ).timeout(const Duration(seconds: 6)); + ).timeout(const Duration(seconds: 30)); final decoded = json.decode(response.body); if (decoded['status'] == 'success' && decoded['data']?['token'] != null) { diff --git a/apps/teacher_app/lib/logic/cubits/teacher_auth_cubit.dart b/apps/teacher_app/lib/logic/cubits/teacher_auth_cubit.dart index 5278130..d31e480 100644 --- a/apps/teacher_app/lib/logic/cubits/teacher_auth_cubit.dart +++ b/apps/teacher_app/lib/logic/cubits/teacher_auth_cubit.dart @@ -86,14 +86,14 @@ class TeacherAuthCubit extends Cubit { try { final res = await repository.verifyOtp(phoneNumber, otp, fullName: fullName); if (res['status'] == 'success') { - if (specialization != null || schoolName != null || gradesTaught != null) { + if ((fullName ?? '').trim().isNotEmpty && (specialization ?? '').trim().isNotEmpty && (schoolName ?? '').trim().isNotEmpty && (gradesTaught?.isNotEmpty ?? false)) { try { await repository.setupProfile( - fullName: fullName ?? 'المعلم المعتمد', - specialization: specialization ?? 'الفيزياء والعلوم التطبيقية', - schoolName: schoolName ?? 'مدرسة الملك عبد الله الثاني للتميز', - gradesTaught: gradesTaught ?? ['الصف العاشر الأساسي'], - bio: 'معلم معتمد في منصة صقل للتعليم التفاعلي', + fullName: fullName!.trim(), + specialization: specialization!.trim(), + schoolName: schoolName!.trim(), + gradesTaught: gradesTaught!, + bio: '', ); } catch (_) {} } diff --git a/apps/teacher_app/lib/presentation/screens/auth/teacher_auth_screen.dart b/apps/teacher_app/lib/presentation/screens/auth/teacher_auth_screen.dart index 6f9c357..da48df8 100644 --- a/apps/teacher_app/lib/presentation/screens/auth/teacher_auth_screen.dart +++ b/apps/teacher_app/lib/presentation/screens/auth/teacher_auth_screen.dart @@ -24,12 +24,12 @@ class TeacherAuthScreen extends StatefulWidget { } class _TeacherAuthScreenState extends State { - final TextEditingController _phoneController = TextEditingController(text: '0798583052'); + final TextEditingController _phoneController = TextEditingController(); final TextEditingController _otpController = TextEditingController(); - final TextEditingController _fullNameController = TextEditingController(text: 'المهندس حمزة الغويريين'); - final TextEditingController _schoolController = TextEditingController(text: 'مدرسة الملك عبد الله الثاني للتميز'); + final TextEditingController _fullNameController = TextEditingController(); + final TextEditingController _schoolController = TextEditingController(); - String _selectedSubject = 'الفيزياء والعلوم التطبيقية'; + String? _selectedSubject; final List _availableSubjects = [ 'الفيزياء والعلوم التطبيقية', 'الرياضيات العلمي', @@ -39,10 +39,7 @@ class _TeacherAuthScreenState extends State { 'اللغة العربية ومشترك التوجيهي', ]; - final Set _selectedGrades = { - 'الصف العاشر الأساسي', - 'الأول ثانوي العلمي', - }; + final Set _selectedGrades = {}; final List _allGrades = [ 'الصف التاسع الأساسي', @@ -79,6 +76,10 @@ class _TeacherAuthScreenState extends State { SaqelToast.showError(context, 'يرجى إدخال رمز التحقق المكون من 6 أرقام'); return; } + if (_fullNameController.text.trim().isEmpty || _selectedSubject == null || _schoolController.text.trim().isEmpty || _selectedGrades.isEmpty) { + SaqelToast.showError(context, 'أكمل الاسم والمادة والمدرسة وصفًا واحدًا على الأقل قبل التحقق'); + return; + } context.read().verifyOtp( phone, otp, diff --git a/apps/teacher_app/lib/presentation/screens/teacher_main_shell.dart b/apps/teacher_app/lib/presentation/screens/teacher_main_shell.dart index bc6e06f..ebc0090 100644 --- a/apps/teacher_app/lib/presentation/screens/teacher_main_shell.dart +++ b/apps/teacher_app/lib/presentation/screens/teacher_main_shell.dart @@ -211,13 +211,13 @@ class _TeacherMainShellState extends State { builder: (context, authState) { TeacherProfileModel profile = widget.initialProfile ?? const TeacherProfileModel( - id: 1, - uuid: 'tch-01', - name: 'المهندس حمزة الغويريين', - specialization: 'الفيزياء والعلوم التطبيقية', - schoolName: 'مدرسة الملك عبد الله الثاني للتميز', - gradesTaught: ['الصف العاشر الأساسي', 'الأول ثانوي العلمي'], - bio: 'معلم معتمد في منصة صَقِل', + id: 0, + uuid: '', + name: 'حساب معلم غير مكتمل', + specialization: 'بانتظار استكمال الملف', + schoolName: '', + gradesTaught: [], + bio: '', ); if (authState is TeacherAuthenticated) { diff --git a/apps/teacher_app/macos/Podfile.lock b/apps/teacher_app/macos/Podfile.lock index f34555e..f394a27 100644 --- a/apps/teacher_app/macos/Podfile.lock +++ b/apps/teacher_app/macos/Podfile.lock @@ -1,6 +1,8 @@ PODS: - device_info_plus (0.0.1): - FlutterMacOS + - file_picker (0.0.1): + - FlutterMacOS - flutter_secure_storage_macos (6.1.3): - FlutterMacOS - FlutterMacOS (1.0.0) @@ -10,6 +12,7 @@ PODS: DEPENDENCIES: - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -17,6 +20,8 @@ DEPENDENCIES: EXTERNAL SOURCES: device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos flutter_secure_storage_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: @@ -26,6 +31,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb diff --git a/backend/app/Controllers/AuthController.php b/backend/app/Controllers/AuthController.php index 74d7531..23635a3 100644 --- a/backend/app/Controllers/AuthController.php +++ b/backend/app/Controllers/AuthController.php @@ -571,6 +571,7 @@ class AuthController $identityId = (int)(is_array($decoded) ? ($decoded['identity_id'] ?? 0) : ($decoded->identity_id ?? 0)); $phone = is_array($decoded) ? ($decoded['phone'] ?? '') : ($decoded->phone ?? ''); + $deviceFingerprint = trim((string)$request->getHeader('x-device-fingerprint', '')) ?: 'browser_default'; // Check if student exists with this National ID $nationalIdHash = Security::blindIndex($nationalId); @@ -591,7 +592,7 @@ class AuthController (int)$student['id'], $student['uuid'], 'student', - 'browser_default', + $deviceFingerprint, $phone, $student['full_name'], $response, @@ -661,6 +662,10 @@ class AuthController $gradeLevel = trim((string)($body['grade_level'] ?? 'grade_10')); $stream = trim((string)($body['stream'] ?? 'scientific')); $nationalId = trim((string)($body['national_id'] ?? '')); + $deviceFingerprint = trim((string)($request->getHeader('x-device-fingerprint', ''))); + if ($deviceFingerprint === '') { + $deviceFingerprint = 'browser_default'; + } if (empty($fullName) || empty($nationalId)) { $response->status(400)->json(['status' => 'error', 'message' => 'الاسم الكامل والرقم الوطني مطلوبان']); @@ -701,7 +706,7 @@ class AuthController } $this->generateSessionAndRespond( - $sId, $sUuid, 'student', 'browser_default', $phone, $fullName, $response, 'تم استكمال التسجيل بنجاح', + $sId, $sUuid, 'student', $deviceFingerprint, $phone, $fullName, $response, 'تم استكمال التسجيل بنجاح', $identityId, (int)(is_array($decoded) ? ($decoded['token_version'] ?? 1) : ($decoded->token_version ?? 1)), null,