Initial commit: Uruk Prize Platform architecture, backend core, mobile app, gateway caller & deployment pipeline
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
class AppConstants {
|
||||
static const String appName = 'جائزة أوروك الدولية';
|
||||
static const String appTagline = 'الهوية الرقمية الموحدة للرعاية الصحية والتعليم';
|
||||
|
||||
// Base API URL (can be customized for production server)
|
||||
static const String defaultBaseUrl = 'https://api.urukprize.iq';
|
||||
|
||||
// Storage Keys
|
||||
static const String keyToken = 'uruk_session_token';
|
||||
static const String keyUserId = 'uruk_user_id';
|
||||
static const String keyDeviceSecret = 'uruk_device_secret';
|
||||
static const String keyFingerprint = 'uruk_device_fingerprint';
|
||||
static const String keySalt = 'uruk_device_salt';
|
||||
static const String keyUserData = 'uruk_user_data';
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../security/fingerprint_service.dart';
|
||||
|
||||
class ApiClient {
|
||||
final String baseUrl;
|
||||
final http.Client _httpClient;
|
||||
final FingerprintService _fingerprintService;
|
||||
|
||||
ApiClient({
|
||||
String? baseUrl,
|
||||
http.Client? httpClient,
|
||||
FingerprintService? fingerprintService,
|
||||
}) : baseUrl = baseUrl ?? AppConstants.defaultBaseUrl,
|
||||
_httpClient = httpClient ?? http.Client(),
|
||||
_fingerprintService = fingerprintService ?? FingerprintService();
|
||||
|
||||
/// Executes an HTTP request with automatic header, device fingerprint, and HMAC signature injection.
|
||||
Future<Map<String, dynamic>> sendRequest({
|
||||
required String method,
|
||||
required String path,
|
||||
Map<String, dynamic>? body,
|
||||
Map<String, String>? queryParams,
|
||||
bool requiresAuth = true,
|
||||
}) async {
|
||||
Uri uri = Uri.parse('$baseUrl$path');
|
||||
if (queryParams != null && queryParams.isNotEmpty) {
|
||||
uri = uri.replace(queryParameters: queryParams);
|
||||
}
|
||||
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
final rawBody = body != null ? jsonEncode(body) : '';
|
||||
|
||||
if (requiresAuth) {
|
||||
final userId = await _fingerprintService.getUserId();
|
||||
final token = await _fingerprintService.getSessionToken();
|
||||
final deviceSecret = await _fingerprintService.getDeviceSecret();
|
||||
final fingerprint = await _fingerprintService.getDeviceFingerprint();
|
||||
|
||||
final timestamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
|
||||
final nonce = const Uuid().v4();
|
||||
|
||||
if (userId != null) {
|
||||
headers['X-User-Id'] = userId.toString();
|
||||
}
|
||||
headers['X-Device-Fingerprint'] = fingerprint;
|
||||
headers['X-Timestamp'] = timestamp;
|
||||
headers['X-Nonce'] = nonce;
|
||||
|
||||
if (token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
// Calculate HMAC-SHA256 signature
|
||||
if (deviceSecret != null) {
|
||||
final bodyHash = sha256.convert(utf8.encode(rawBody)).toString();
|
||||
final signaturePayload = '$method\n$path\n$timestamp\n$nonce\n$bodyHash';
|
||||
|
||||
final hmacSha256 = Hmac(sha256, utf8.encode(deviceSecret));
|
||||
final signature = hmacSha256.convert(utf8.encode(signaturePayload)).toString();
|
||||
headers['X-Signature'] = signature;
|
||||
}
|
||||
}
|
||||
|
||||
http.Response response;
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET':
|
||||
response = await _httpClient.get(uri, headers: headers);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await _httpClient.post(uri, headers: headers, body: rawBody.isNotEmpty ? rawBody : null);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await _httpClient.put(uri, headers: headers, body: rawBody.isNotEmpty ? rawBody : null);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await _httpClient.delete(uri, headers: headers, body: rawBody.isNotEmpty ? rawBody : null);
|
||||
break;
|
||||
default:
|
||||
throw UnsupportedError('HTTP method $method not supported');
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return decoded;
|
||||
} else {
|
||||
final msg = decoded['message'] ?? 'Request failed with status ${response.statusCode}';
|
||||
throw ApiException(msg, response.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> get(String path, {Map<String, String>? queryParams, bool requiresAuth = true}) {
|
||||
return sendRequest(method: 'GET', path: path, queryParams: queryParams, requiresAuth: requiresAuth);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> post(String path, {Map<String, dynamic>? body, bool requiresAuth = true}) {
|
||||
return sendRequest(method: 'POST', path: path, body: body, requiresAuth: requiresAuth);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiException implements Exception {
|
||||
final String message;
|
||||
final int statusCode;
|
||||
|
||||
ApiException(this.message, this.statusCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ApiException [$statusCode]: $message';
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
|
||||
class FingerprintService {
|
||||
final FlutterSecureStorage _storage;
|
||||
final DeviceInfoPlugin _deviceInfo;
|
||||
static const MethodChannel _nativeChannel = MethodChannel('iq.urukprize.uruk_prize/device_fingerprint');
|
||||
|
||||
FingerprintService({
|
||||
FlutterSecureStorage? storage,
|
||||
DeviceInfoPlugin? deviceInfo,
|
||||
}) : _storage = storage ?? const FlutterSecureStorage(),
|
||||
_deviceInfo = deviceInfo ?? DeviceInfoPlugin();
|
||||
|
||||
String? _cachedFingerprint;
|
||||
|
||||
/// Retrieves or generates a tamper-proof hardware-bound device fingerprint.
|
||||
/// Prioritizes Native MethodChannel (Kotlin on Android / Swift on iOS with Keychain persistence).
|
||||
Future<String> getDeviceFingerprint() async {
|
||||
if (_cachedFingerprint != null) {
|
||||
return _cachedFingerprint!;
|
||||
}
|
||||
|
||||
// 1. Check if previously generated and stored securely
|
||||
String? storedFp = await _storage.read(key: AppConstants.keyFingerprint);
|
||||
if (storedFp != null && storedFp.length == 64) {
|
||||
_cachedFingerprint = storedFp;
|
||||
return storedFp;
|
||||
}
|
||||
|
||||
// 2. Fetch or create persistent random cryptographic salt
|
||||
String? salt = await _storage.read(key: AppConstants.keySalt);
|
||||
if (salt == null) {
|
||||
salt = const Uuid().v4() + DateTime.now().microsecondsSinceEpoch.toString();
|
||||
await _storage.write(key: AppConstants.keySalt, value: salt);
|
||||
}
|
||||
|
||||
// 3. Extract hardware parameters via Native MethodChannel first
|
||||
String hardwareData = '';
|
||||
try {
|
||||
final String? nativeRaw = await _nativeChannel.invokeMethod<String>('getHardwareRawData');
|
||||
if (nativeRaw != null && nativeRaw.isNotEmpty) {
|
||||
hardwareData = nativeRaw;
|
||||
}
|
||||
} catch (_) {
|
||||
// MethodChannel fallback to device_info_plus
|
||||
}
|
||||
|
||||
if (hardwareData.isEmpty) {
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await _deviceInfo.androidInfo;
|
||||
hardwareData = '${androidInfo.brand}-${androidInfo.model}-${androidInfo.id}-${androidInfo.hardware}-${androidInfo.manufacturer}';
|
||||
} else if (Platform.isIOS) {
|
||||
final iosInfo = await _deviceInfo.iosInfo;
|
||||
hardwareData = '${iosInfo.name}-${iosInfo.model}-${iosInfo.systemName}-${iosInfo.identifierForVendor}';
|
||||
} else {
|
||||
hardwareData = 'generic-client-uruk';
|
||||
}
|
||||
} catch (_) {
|
||||
hardwareData = 'fallback-hardware-id';
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Generate SHA-256 Digest
|
||||
final combinedBytes = utf8.encode('$hardwareData::$salt::uruk_prize_secure');
|
||||
final digest = sha256.convert(combinedBytes);
|
||||
final fingerprint = digest.toString();
|
||||
|
||||
// 5. Store permanently
|
||||
await _storage.write(key: AppConstants.keyFingerprint, value: fingerprint);
|
||||
_cachedFingerprint = fingerprint;
|
||||
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
Future<String> getDeviceModel() async {
|
||||
try {
|
||||
final String? nativeModel = await _nativeChannel.invokeMethod<String>('getDeviceModel');
|
||||
if (nativeModel != null && nativeModel.isNotEmpty) {
|
||||
return nativeModel;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
final info = await _deviceInfo.androidInfo;
|
||||
return '${info.manufacturer} ${info.model}';
|
||||
} else if (Platform.isIOS) {
|
||||
final info = await _deviceInfo.iosInfo;
|
||||
return info.utsname.machine;
|
||||
}
|
||||
} catch (_) {}
|
||||
return 'Unknown Device';
|
||||
}
|
||||
|
||||
Future<void> saveAuthSession({
|
||||
required int userId,
|
||||
required String token,
|
||||
required String deviceSecret,
|
||||
}) async {
|
||||
await _storage.write(key: AppConstants.keyUserId, value: userId.toString());
|
||||
await _storage.write(key: AppConstants.keyToken, value: token);
|
||||
await _storage.write(key: AppConstants.keyDeviceSecret, value: deviceSecret);
|
||||
}
|
||||
|
||||
Future<int?> getUserId() async {
|
||||
final val = await _storage.read(key: AppConstants.keyUserId);
|
||||
return val != null ? int.tryParse(val) : null;
|
||||
}
|
||||
|
||||
Future<String?> getSessionToken() async {
|
||||
return await _storage.read(key: AppConstants.keyToken);
|
||||
}
|
||||
|
||||
Future<String?> getDeviceSecret() async {
|
||||
return await _storage.read(key: AppConstants.keyDeviceSecret);
|
||||
}
|
||||
|
||||
Future<void> clearSession() async {
|
||||
await _storage.delete(key: AppConstants.keyUserId);
|
||||
await _storage.delete(key: AppConstants.keyToken);
|
||||
await _storage.delete(key: AppConstants.keyDeviceSecret);
|
||||
await _storage.delete(key: AppConstants.keyUserData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
// Primary Palette
|
||||
static const Color primaryDark = Color(0xFF0F172A); // Midnight Navy
|
||||
static const Color cardDark = Color(0xFF1E293B); // Slate Navy
|
||||
static const Color goldAccent = Color(0xFFD4AF37); // Uruk Metallic Gold
|
||||
static const Color goldLight = Color(0xFFF3E5AB); // Pale Gold
|
||||
static const Color emeraldGreen = Color(0xFF10B981);// Active Green
|
||||
static const Color crimsonRed = Color(0xFFEF4444); // Expired / Warning Red
|
||||
static const Color textMuted = Color(0xFF94A3B8); // Muted Slate Text
|
||||
|
||||
static ThemeData get darkTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: primaryDark,
|
||||
primaryColor: goldAccent,
|
||||
cardColor: cardDark,
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: primaryDark,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
titleTextStyle: TextStyle(
|
||||
color: goldLight,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: goldAccent,
|
||||
secondary: goldLight,
|
||||
surface: cardDark,
|
||||
),
|
||||
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: goldAccent,
|
||||
foregroundColor: primaryDark,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user