From 5b7cd2b411509a18387bfaab738e6729f37c738f Mon Sep 17 00:00:00 2001 From: Devanarayanan Date: Sat, 23 May 2026 15:04:38 +0000 Subject: [PATCH] refactor: increase network and operation timeouts to 30 seconds and update security error messaging --- .example.env | 4 ++-- mobile/lib/config/app_config.dart | 2 +- mobile/lib/main.dart | 2 +- mobile/lib/providers/auth_provider.dart | 6 +++--- mobile/lib/screens/attendance_calendar_screen.dart | 2 +- mobile/lib/screens/dashboard_screen.dart | 2 +- mobile/lib/screens/navigation_shell.dart | 2 +- mobile/lib/screens/tracking_screen.dart | 2 +- mobile/lib/services/api_service.dart | 4 ++-- mobile/lib/services/dio_service.dart | 6 +++--- mobile/lib/services/jwe_service.dart | 2 +- mobile/pubspec.yaml | 2 +- package-lock.json | 4 ++-- package.json | 2 +- public/openapi/openapi.yaml | 2 +- src/lib/security/app-check.ts | 8 ++++---- 16 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.example.env b/.example.env index 43148b57..b969e7b7 100644 --- a/.example.env +++ b/.example.env @@ -44,7 +44,7 @@ NEXT_PUBLIC_APP_NAME=GhostClass # âš ī¸ App version displayed in footer and health checks # 🔨 Build-time (Infisical `/build-time` folder) -NEXT_PUBLIC_APP_VERSION=4.3.9 +NEXT_PUBLIC_APP_VERSION=4.4.0 # âš ī¸ Your production domain WITHOUT https:// # All URL-based variables are derived from this. @@ -359,7 +359,7 @@ JWE_PRIVATE_KEY= # âš ī¸ Minimum supported app version required to bypass forced update # 🚀 Runtime (Infisical `/runtime` folder → Server Env Var) -MIN_APP_VERSION=4.3.9 +MIN_APP_VERSION=4.4.0 # â„šī¸ Enforce Firebase App Check for all mobile clients in production # Valid: "true", "false" (default: false in dev, true recommended in prod) diff --git a/mobile/lib/config/app_config.dart b/mobile/lib/config/app_config.dart index cd19d7df..4f10d66c 100644 --- a/mobile/lib/config/app_config.dart +++ b/mobile/lib/config/app_config.dart @@ -75,7 +75,7 @@ class AppConfig { /// Current application version (derived from Infisical compilation injection). static String get appVersion => - const String.fromEnvironment('APP_VERSION', defaultValue: '4.3.9'); + const String.fromEnvironment('APP_VERSION', defaultValue: '4.4.0'); /// Commit SHA injected by CI for release builds. static String get appCommitSha => diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 05470d9b..c143c802 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -32,7 +32,7 @@ class MyHttpOverrides extends HttpOverrides { final client = super.createHttpClient(context) ..connectionTimeout = kDebugMode ? const Duration(seconds: 40) - : const Duration(seconds: 20); + : const Duration(seconds: 30); // In debug mode, we allow untrusted certificates ONLY if they match our expected hostname. // In release mode, standard certificate validation is enforced. diff --git a/mobile/lib/providers/auth_provider.dart b/mobile/lib/providers/auth_provider.dart index 652d3701..f35cc6e1 100644 --- a/mobile/lib/providers/auth_provider.dart +++ b/mobile/lib/providers/auth_provider.dart @@ -278,7 +278,7 @@ class AuthNotifier extends AsyncNotifier ); syncRes = await api .syncMobileAuth(supabaseToken) - .timeout(const Duration(seconds: 10)); + .timeout(const Duration(seconds: 30)); } on TimeoutException catch (e, st) { AppLogger.e( 'AuthNotifier [HEAL-$healAttemptId]: syncMobileAuth timed out (attempt 1)', @@ -297,7 +297,7 @@ class AuthNotifier extends AsyncNotifier ); syncRes = await api .syncMobileAuth(supabaseToken) - .timeout(const Duration(seconds: 10)); + .timeout(const Duration(seconds: 30)); } on Object catch (e, st) { AppLogger.e( 'AuthNotifier [HEAL-$healAttemptId]: syncMobileAuth retry failed', @@ -1452,7 +1452,7 @@ class AuthNotifier extends AsyncNotifier .read(supabaseClientProvider) .auth .refreshSession() - .timeout(const Duration(seconds: 10)); + .timeout(const Duration(seconds: 30)); return res.session?.accessToken; } return session.accessToken; diff --git a/mobile/lib/screens/attendance_calendar_screen.dart b/mobile/lib/screens/attendance_calendar_screen.dart index fc57b9f8..95467160 100644 --- a/mobile/lib/screens/attendance_calendar_screen.dart +++ b/mobile/lib/screens/attendance_calendar_screen.dart @@ -126,7 +126,7 @@ class _AttendanceCalendarScreenState ref.read(dashboardProvider.future), ref.read(trackingProvider.future), ref.read(academicProvider.future), - ]).timeout(const Duration(seconds: 10)); + ]).timeout(const Duration(seconds: 30)); } on Object catch (e, st) { AppLogger.e('AttendanceCalendarScreen: Retry failed', e, st); } diff --git a/mobile/lib/screens/dashboard_screen.dart b/mobile/lib/screens/dashboard_screen.dart index 0ee8427f..2857ef4b 100644 --- a/mobile/lib/screens/dashboard_screen.dart +++ b/mobile/lib/screens/dashboard_screen.dart @@ -81,7 +81,7 @@ class _DashboardScreenState extends ConsumerState { try { await ref .read(dashboardProvider.future) - .timeout(const Duration(seconds: 10)); + .timeout(const Duration(seconds: 30)); } on Object catch (e, st) { AppLogger.e('DashboardScreen: Retry failed', e, st); } diff --git a/mobile/lib/screens/navigation_shell.dart b/mobile/lib/screens/navigation_shell.dart index 02a529cb..ccf1ca61 100644 --- a/mobile/lib/screens/navigation_shell.dart +++ b/mobile/lib/screens/navigation_shell.dart @@ -664,7 +664,7 @@ class _NavigationShellState extends ConsumerState { await Future.wait([ ref.read(dashboardProvider.future), ref.read(trackingProvider.future), - ]).timeout(const Duration(seconds: 10)); + ]).timeout(const Duration(seconds: 30)); AppLogger.i( 'NavigationShell: Outage recovery wait completed (or partial success).', ); diff --git a/mobile/lib/screens/tracking_screen.dart b/mobile/lib/screens/tracking_screen.dart index ce5f90ab..37459ebe 100644 --- a/mobile/lib/screens/tracking_screen.dart +++ b/mobile/lib/screens/tracking_screen.dart @@ -62,7 +62,7 @@ class _TrackingScreenState extends ConsumerState try { await ref .read(trackingProvider.future) - .timeout(const Duration(seconds: 10)); + .timeout(const Duration(seconds: 30)); } on Object catch (e, st) { AppLogger.e('TrackingScreen: Retry failed', e, st); } diff --git a/mobile/lib/services/api_service.dart b/mobile/lib/services/api_service.dart index 2d34c544..dc949e30 100644 --- a/mobile/lib/services/api_service.dart +++ b/mobile/lib/services/api_service.dart @@ -176,8 +176,8 @@ class ApiService { '${AppConfig.ghostclassApiUrl}/cron/sync?t=${now.millisecondsSinceEpoch}', options: Options( headers: {'Authorization': 'Bearer $supabaseToken'}, - sendTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 15), + sendTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), ), ); _lastSyncTime = DateTime.now(); diff --git a/mobile/lib/services/dio_service.dart b/mobile/lib/services/dio_service.dart index 29404c16..c1a1f910 100644 --- a/mobile/lib/services/dio_service.dart +++ b/mobile/lib/services/dio_service.dart @@ -16,7 +16,7 @@ import 'package:sentry_dio/sentry_dio.dart'; /// Configures interceptors for JWE, Sentry, and authentication headers. class DioService { DioService(this._ref) { - const timeout = kDebugMode ? Duration(seconds: 40) : Duration(seconds: 20); + const timeout = kDebugMode ? Duration(seconds: 40) : Duration(seconds: 30); dio = Dio( BaseOptions( @@ -137,7 +137,7 @@ class DioService { isNew = true; } appCheckToken = await _limitedTokenFetchInFlight!.timeout( - const Duration(seconds: 10), + const Duration(seconds: 30), ); if (isNew) { AppLogger.safeUnawait( @@ -162,7 +162,7 @@ class DioService { isNew = true; } appCheckToken = await _tokenFetchInFlight!.timeout( - const Duration(seconds: 10), + const Duration(seconds: 30), ); if (isNew) { AppLogger.safeUnawait( diff --git a/mobile/lib/services/jwe_service.dart b/mobile/lib/services/jwe_service.dart index 9351bf70..5b9bf069 100644 --- a/mobile/lib/services/jwe_service.dart +++ b/mobile/lib/services/jwe_service.dart @@ -22,7 +22,7 @@ class JweService { JweService._internal() { const networkTimeout = kDebugMode ? Duration(seconds: 40) - : Duration(seconds: 20); + : Duration(seconds: 30); _dio = Dio( BaseOptions( diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a921da2d..85c90935 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 4.3.9+1 +version: 4.4.0+1 environment: sdk: ^3.11.4 diff --git a/package-lock.json b/package-lock.json index f6b96f55..c318b77a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostclass", - "version": "4.3.9", + "version": "4.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostclass", - "version": "4.3.9", + "version": "4.4.0", "dependencies": { "@hookform/resolvers": "^5.2.2", "@radix-ui/react-alert-dialog": "^1.1.15", diff --git a/package.json b/package.json index 8f69d556..8dfa2430 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostclass", - "version": "4.3.9", + "version": "4.4.0", "private": true, "engines": { "node": ">=22.12.0", diff --git a/public/openapi/openapi.yaml b/public/openapi/openapi.yaml index 750fb6b4..129a1fa1 100644 --- a/public/openapi/openapi.yaml +++ b/public/openapi/openapi.yaml @@ -6,7 +6,7 @@ openapi: 3.1.0 info: title: GhostClass API - version: 4.3.9 + version: 4.4.0 description: | **GhostClass API** provides endpoints for authentication, profile synchronization, attendance integrations with EzyGo, telemetry, and build provenance. diff --git a/src/lib/security/app-check.ts b/src/lib/security/app-check.ts index 192061ed..563f1fbd 100644 --- a/src/lib/security/app-check.ts +++ b/src/lib/security/app-check.ts @@ -195,8 +195,8 @@ async function verifyCsrfAuth( const res = await verifyCsrfTokenWithSessionBinding(headerList, sessionId); if (!res.isValid) { return { - isValid: false, error: res.error, reason: "Web security check failed.", - action: "Please refresh your browser.", authType: "csrf", isWebRequest: true, + isValid: false, error: res.error, reason: "Security check failed.", + action: "Please refresh the page or restart the app.", authType: "csrf", isWebRequest: true, }; } return { isValid: true, authType: "csrf", isWebRequest: true }; @@ -238,8 +238,8 @@ async function verifyAuthentication( return { isValid: false, error: "Missing CSRF token", - reason: "Web security check failed.", - action: "Please refresh your browser.", + reason: "Security check failed.", + action: "Please refresh the page or restart the app.", authType: "csrf", isWebRequest: true, };