Update Saqel Platform: 2026-09-08 17:51:07
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user