Update Saqel Platform: 2026-09-08 17:51:07

This commit is contained in:
Hamza-Ayed
2026-09-08 17:51:07 +03:00
parent 902cf00152
commit 75451ebc3f
12 changed files with 84 additions and 39 deletions
@@ -6,6 +6,9 @@ import '../utils/app_logger.dart';
/// Enterprise Storage Service with Device-Bound Payload Obfuscation & Secure KeyStore/Keychain /// Enterprise Storage Service with Device-Bound Payload Obfuscation & Secure KeyStore/Keychain
class StorageService { 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<String, String> _volatileStore = <String, String>{};
final FlutterSecureStorage _secureStorage; final FlutterSecureStorage _secureStorage;
StorageService({FlutterSecureStorage? secureStorage}) StorageService({FlutterSecureStorage? secureStorage})
@@ -26,6 +29,7 @@ class StorageService {
static const String _keyDeviceId = 'saqel_device_id'; static const String _keyDeviceId = 'saqel_device_id';
Future<void> _writeSafe(String key, String value) async { Future<void> _writeSafe(String key, String value) async {
_volatileStore[key] = value;
try { try {
await _secureStorage.write(key: key, value: value); await _secureStorage.write(key: key, value: value);
} on PlatformException catch (e) { } on PlatformException catch (e) {
@@ -38,10 +42,11 @@ class StorageService {
final val = await _secureStorage.read(key: key); final val = await _secureStorage.read(key: key);
if (val != null && val.isNotEmpty) return val; if (val != null && val.isNotEmpty) return val;
} catch (_) {} } catch (_) {}
return null; return _volatileStore[key];
} }
Future<void> _deleteSafe(String key) async { Future<void> _deleteSafe(String key) async {
_volatileStore.remove(key);
try { try {
await _secureStorage.delete(key: key); await _secureStorage.delete(key: key);
} catch (_) {} } catch (_) {}
@@ -88,7 +88,7 @@ class AuthRepository {
required String identityToken, required String identityToken,
required String fullName, required String fullName,
required String nationalId, required String nationalId,
String gradeLevel = 'tawjihi_2008', String gradeLevel = 'grade_10',
String stream = 'scientific', String stream = 'scientific',
}) async { }) async {
final res = await _api.post( final res = await _api.post(
@@ -148,7 +148,7 @@ class AuthCubit extends Cubit<AuthState> {
required String identityToken, required String identityToken,
required String fullName, required String fullName,
required String nationalId, required String nationalId,
String gradeLevel = 'tawjihi_2008', String gradeLevel = 'grade_10',
String stream = 'scientific', String stream = 'scientific',
}) async { }) async {
AppLogger.log('Setting up profile for $fullName ($nationalId)...', tag: 'AUTH_CUBIT'); AppLogger.log('Setting up profile for $fullName ($nationalId)...', tag: 'AUTH_CUBIT');
@@ -23,7 +23,7 @@ class _AuthScreenState extends State<AuthScreen> {
final TextEditingController _fullNameController = TextEditingController(); final TextEditingController _fullNameController = TextEditingController();
String _selectedRole = 'student'; String _selectedRole = 'student';
final String _selectedGrade = 'tawjihi_2008'; String _selectedGrade = 'grade_10';
String _selectedStream = 'scientific'; String _selectedStream = 'scientific';
@override @override
@@ -440,7 +440,30 @@ class _AuthScreenState extends State<AuthScreen> {
), ),
const SizedBox(height: 18), 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( Text(
'الفرع الأكاديمي', 'الفرع الأكاديمي',
style: AppTypography.titleMedium.copyWith(color: AppColors.textSecondaryDark, fontSize: 13), style: AppTypography.titleMedium.copyWith(color: AppColors.textSecondaryDark, fontSize: 13),
@@ -501,6 +524,7 @@ class _AuthScreenState extends State<AuthScreen> {
), ),
], ],
), ),
],
const SizedBox(height: 24), const SizedBox(height: 24),
LuxuryButton( LuxuryButton(
@@ -4,6 +4,7 @@ import '../utils/app_logger.dart';
/// Ultra-Resilient Storage Service for Teacher Studio /// Ultra-Resilient Storage Service for Teacher Studio
class StorageService { class StorageService {
static final Map<String, String> _volatileStore = <String, String>{};
final FlutterSecureStorage _secureStorage; final FlutterSecureStorage _secureStorage;
StorageService({FlutterSecureStorage? secureStorage}) StorageService({FlutterSecureStorage? secureStorage})
@@ -21,6 +22,7 @@ class StorageService {
static const String _keyUser = 'saqel_teacher_user_data'; static const String _keyUser = 'saqel_teacher_user_data';
Future<void> _writeSafe(String key, String value) async { Future<void> _writeSafe(String key, String value) async {
_volatileStore[key] = value;
try { try {
await _secureStorage.write(key: key, value: value); await _secureStorage.write(key: key, value: value);
} on PlatformException catch (e) { } on PlatformException catch (e) {
@@ -34,13 +36,15 @@ class StorageService {
if (val != null && val.isNotEmpty) return val; if (val != null && val.isNotEmpty) return val;
} catch (_) {} } catch (_) {}
return null; return _volatileStore[key];
} }
Future<void> saveToken(String token) async => await _writeSafe(_keyToken, token); Future<void> saveToken(String token) async => await _writeSafe(_keyToken, token);
Future<String?> getToken() async => await _readSafe(_keyToken); Future<String?> getToken() async => await _readSafe(_keyToken);
Future<void> clearSession() async { Future<void> clearSession() async {
_volatileStore.remove(_keyToken);
_volatileStore.remove(_keyUser);
try { try {
await _secureStorage.delete(key: _keyToken); await _secureStorage.delete(key: _keyToken);
await _secureStorage.delete(key: _keyUser); await _secureStorage.delete(key: _keyUser);
@@ -184,20 +184,20 @@ class TeacherProfileModel {
grades = [profile['grades_taught'].toString()]; grades = [profile['grades_taught'].toString()];
} }
if (grades.isEmpty) { if (grades.isEmpty) {
grades = ['الصف العاشر الأساسي', 'الأول ثانوي العلمي']; grades = [];
} }
return TeacherProfileModel( return TeacherProfileModel(
id: user['id'] is int ? user['id'] : 1, id: user['id'] is int ? user['id'] : 1,
uuid: user['uuid']?.toString() ?? '', uuid: user['uuid']?.toString() ?? '',
name: user['full_name']?.toString() ?? 'الأستاذ المعتمد', name: user['full_name']?.toString() ?? 'حساب معلم غير مكتمل',
specialization: profile['specialization']?.toString() ?? 'الفيزياء', specialization: profile['specialization']?.toString() ?? 'بانتظار استكمال التخصص',
schoolName: profile['school_name']?.toString() ?? 'مدرسة الملك عبد الله الثاني للتميز', schoolName: profile['school_name']?.toString() ?? '',
gradesTaught: grades, gradesTaught: grades,
bio: profile['bio']?.toString() ?? 'معلم معتمد في منصة صَقِل', bio: profile['bio']?.toString() ?? '',
rating: 4.9, rating: 0,
studentsCount: 42, studentsCount: 0,
lessonsCount: 28, lessonsCount: 0,
); );
} }
@@ -56,7 +56,7 @@ class TeacherRepository {
'phone_number': phoneNumber, 'phone_number': phoneNumber,
'role': 'teacher', 'role': 'teacher',
}), }),
).timeout(const Duration(seconds: 6)); ).timeout(const Duration(seconds: 30));
return json.decode(response.body); return json.decode(response.body);
} catch (e) { } catch (e) {
@@ -78,7 +78,7 @@ class TeacherRepository {
if (fullName != null) 'full_name': fullName, if (fullName != null) 'full_name': fullName,
'device_fingerprint': 'saqel_teacher_flutter', 'device_fingerprint': 'saqel_teacher_flutter',
}), }),
).timeout(const Duration(seconds: 6)); ).timeout(const Duration(seconds: 30));
final decoded = json.decode(response.body); final decoded = json.decode(response.body);
if (decoded['status'] == 'success' && decoded['data']?['token'] != null) { if (decoded['status'] == 'success' && decoded['data']?['token'] != null) {
@@ -86,14 +86,14 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
try { try {
final res = await repository.verifyOtp(phoneNumber, otp, fullName: fullName); final res = await repository.verifyOtp(phoneNumber, otp, fullName: fullName);
if (res['status'] == 'success') { 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 { try {
await repository.setupProfile( await repository.setupProfile(
fullName: fullName ?? 'المعلم المعتمد', fullName: fullName!.trim(),
specialization: specialization ?? 'الفيزياء والعلوم التطبيقية', specialization: specialization!.trim(),
schoolName: schoolName ?? 'مدرسة الملك عبد الله الثاني للتميز', schoolName: schoolName!.trim(),
gradesTaught: gradesTaught ?? ['الصف العاشر الأساسي'], gradesTaught: gradesTaught!,
bio: 'معلم معتمد في منصة صقل للتعليم التفاعلي', bio: '',
); );
} catch (_) {} } catch (_) {}
} }
@@ -24,12 +24,12 @@ class TeacherAuthScreen extends StatefulWidget {
} }
class _TeacherAuthScreenState extends State<TeacherAuthScreen> { class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
final TextEditingController _phoneController = TextEditingController(text: '0798583052'); final TextEditingController _phoneController = TextEditingController();
final TextEditingController _otpController = TextEditingController(); final TextEditingController _otpController = TextEditingController();
final TextEditingController _fullNameController = TextEditingController(text: 'المهندس حمزة الغويريين'); final TextEditingController _fullNameController = TextEditingController();
final TextEditingController _schoolController = TextEditingController(text: 'مدرسة الملك عبد الله الثاني للتميز'); final TextEditingController _schoolController = TextEditingController();
String _selectedSubject = 'الفيزياء والعلوم التطبيقية'; String? _selectedSubject;
final List<String> _availableSubjects = [ final List<String> _availableSubjects = [
'الفيزياء والعلوم التطبيقية', 'الفيزياء والعلوم التطبيقية',
'الرياضيات العلمي', 'الرياضيات العلمي',
@@ -39,10 +39,7 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
'اللغة العربية ومشترك التوجيهي', 'اللغة العربية ومشترك التوجيهي',
]; ];
final Set<String> _selectedGrades = { final Set<String> _selectedGrades = {};
'الصف العاشر الأساسي',
'الأول ثانوي العلمي',
};
final List<String> _allGrades = [ final List<String> _allGrades = [
'الصف التاسع الأساسي', 'الصف التاسع الأساسي',
@@ -79,6 +76,10 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
SaqelToast.showError(context, 'يرجى إدخال رمز التحقق المكون من 6 أرقام'); SaqelToast.showError(context, 'يرجى إدخال رمز التحقق المكون من 6 أرقام');
return; return;
} }
if (_fullNameController.text.trim().isEmpty || _selectedSubject == null || _schoolController.text.trim().isEmpty || _selectedGrades.isEmpty) {
SaqelToast.showError(context, 'أكمل الاسم والمادة والمدرسة وصفًا واحدًا على الأقل قبل التحقق');
return;
}
context.read<TeacherAuthCubit>().verifyOtp( context.read<TeacherAuthCubit>().verifyOtp(
phone, phone,
otp, otp,
@@ -211,13 +211,13 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
builder: (context, authState) { builder: (context, authState) {
TeacherProfileModel profile = widget.initialProfile ?? TeacherProfileModel profile = widget.initialProfile ??
const TeacherProfileModel( const TeacherProfileModel(
id: 1, id: 0,
uuid: 'tch-01', uuid: '',
name: 'المهندس حمزة الغويريين', name: 'حساب معلم غير مكتمل',
specialization: 'الفيزياء والعلوم التطبيقية', specialization: 'بانتظار استكمال الملف',
schoolName: 'مدرسة الملك عبد الله الثاني للتميز', schoolName: '',
gradesTaught: ['الصف العاشر الأساسي', 'الأول ثانوي العلمي'], gradesTaught: [],
bio: 'معلم معتمد في منصة صَقِل', bio: '',
); );
if (authState is TeacherAuthenticated) { if (authState is TeacherAuthenticated) {
+6
View File
@@ -1,6 +1,8 @@
PODS: PODS:
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- flutter_secure_storage_macos (6.1.3): - flutter_secure_storage_macos (6.1.3):
- FlutterMacOS - FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
@@ -10,6 +12,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - 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`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -17,6 +20,8 @@ DEPENDENCIES:
EXTERNAL SOURCES: EXTERNAL SOURCES:
device_info_plus: device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
flutter_secure_storage_macos: flutter_secure_storage_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
FlutterMacOS: FlutterMacOS:
@@ -26,6 +31,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
+7 -2
View File
@@ -571,6 +571,7 @@ class AuthController
$identityId = (int)(is_array($decoded) ? ($decoded['identity_id'] ?? 0) : ($decoded->identity_id ?? 0)); $identityId = (int)(is_array($decoded) ? ($decoded['identity_id'] ?? 0) : ($decoded->identity_id ?? 0));
$phone = is_array($decoded) ? ($decoded['phone'] ?? '') : ($decoded->phone ?? ''); $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 // Check if student exists with this National ID
$nationalIdHash = Security::blindIndex($nationalId); $nationalIdHash = Security::blindIndex($nationalId);
@@ -591,7 +592,7 @@ class AuthController
(int)$student['id'], (int)$student['id'],
$student['uuid'], $student['uuid'],
'student', 'student',
'browser_default', $deviceFingerprint,
$phone, $phone,
$student['full_name'], $student['full_name'],
$response, $response,
@@ -661,6 +662,10 @@ class AuthController
$gradeLevel = trim((string)($body['grade_level'] ?? 'grade_10')); $gradeLevel = trim((string)($body['grade_level'] ?? 'grade_10'));
$stream = trim((string)($body['stream'] ?? 'scientific')); $stream = trim((string)($body['stream'] ?? 'scientific'));
$nationalId = trim((string)($body['national_id'] ?? '')); $nationalId = trim((string)($body['national_id'] ?? ''));
$deviceFingerprint = trim((string)($request->getHeader('x-device-fingerprint', '')));
if ($deviceFingerprint === '') {
$deviceFingerprint = 'browser_default';
}
if (empty($fullName) || empty($nationalId)) { if (empty($fullName) || empty($nationalId)) {
$response->status(400)->json(['status' => 'error', 'message' => 'الاسم الكامل والرقم الوطني مطلوبان']); $response->status(400)->json(['status' => 'error', 'message' => 'الاسم الكامل والرقم الوطني مطلوبان']);
@@ -701,7 +706,7 @@ class AuthController
} }
$this->generateSessionAndRespond( $this->generateSessionAndRespond(
$sId, $sUuid, 'student', 'browser_default', $phone, $fullName, $response, 'تم استكمال التسجيل بنجاح', $sId, $sUuid, 'student', $deviceFingerprint, $phone, $fullName, $response, 'تم استكمال التسجيل بنجاح',
$identityId, $identityId,
(int)(is_array($decoded) ? ($decoded['token_version'] ?? 1) : ($decoded->token_version ?? 1)), (int)(is_array($decoded) ? ($decoded['token_version'] ?? 1) : ($decoded->token_version ?? 1)),
null, null,