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
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;
StorageService({FlutterSecureStorage? secureStorage})
@@ -26,6 +29,7 @@ class StorageService {
static const String _keyDeviceId = 'saqel_device_id';
Future<void> _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<void> _deleteSafe(String key) async {
_volatileStore.remove(key);
try {
await _secureStorage.delete(key: key);
} catch (_) {}
@@ -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(
@@ -148,7 +148,7 @@ class AuthCubit extends Cubit<AuthState> {
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');
@@ -23,7 +23,7 @@ class _AuthScreenState extends State<AuthScreen> {
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<AuthScreen> {
),
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<AuthScreen> {
),
],
),
],
const SizedBox(height: 24),
LuxuryButton(
@@ -4,6 +4,7 @@ import '../utils/app_logger.dart';
/// Ultra-Resilient Storage Service for Teacher Studio
class StorageService {
static final Map<String, String> _volatileStore = <String, String>{};
final FlutterSecureStorage _secureStorage;
StorageService({FlutterSecureStorage? secureStorage})
@@ -21,6 +22,7 @@ class StorageService {
static const String _keyUser = 'saqel_teacher_user_data';
Future<void> _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<void> saveToken(String token) async => await _writeSafe(_keyToken, token);
Future<String?> getToken() async => await _readSafe(_keyToken);
Future<void> clearSession() async {
_volatileStore.remove(_keyToken);
_volatileStore.remove(_keyUser);
try {
await _secureStorage.delete(key: _keyToken);
await _secureStorage.delete(key: _keyUser);
@@ -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,
);
}
@@ -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) {
@@ -86,14 +86,14 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
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 (_) {}
}
@@ -24,12 +24,12 @@ class TeacherAuthScreen extends StatefulWidget {
}
class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
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<String> _availableSubjects = [
'الفيزياء والعلوم التطبيقية',
'الرياضيات العلمي',
@@ -39,10 +39,7 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
'اللغة العربية ومشترك التوجيهي',
];
final Set<String> _selectedGrades = {
'الصف العاشر الأساسي',
'الأول ثانوي العلمي',
};
final Set<String> _selectedGrades = {};
final List<String> _allGrades = [
'الصف التاسع الأساسي',
@@ -79,6 +76,10 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
SaqelToast.showError(context, 'يرجى إدخال رمز التحقق المكون من 6 أرقام');
return;
}
if (_fullNameController.text.trim().isEmpty || _selectedSubject == null || _schoolController.text.trim().isEmpty || _selectedGrades.isEmpty) {
SaqelToast.showError(context, 'أكمل الاسم والمادة والمدرسة وصفًا واحدًا على الأقل قبل التحقق');
return;
}
context.read<TeacherAuthCubit>().verifyOtp(
phone,
otp,
@@ -211,13 +211,13 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
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) {
+6
View File
@@ -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
+7 -2
View File
@@ -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,