From bc12fe70aa0d263df830dc21eb24097faa17deba Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Tue, 31 Mar 2026 07:15:02 +0530 Subject: [PATCH] security: sanitize all error messages shown to users Created centralized userFriendlyError() utility that converts raw exceptions into clean, user-friendly messages. Strips hostnames, ports, OS error codes, HTTP status codes, stack traces, and Django field names. Maps network/timeout/auth/server errors to plain English messages. Fixed 16 locations across 10 files: - home_screen, calendar_screen, learn_more_screen (SnackBar/Text) - login_screen, desktop_login_screen (SnackBar) - profile_screen, contribute_screen, search_screen (SnackBar) - review_form, review_section (inline error text) - gamification_provider (error field) Also removed double-wrapped exceptions in ReviewService (rethrow instead of throw Exception('Failed to...: $e')). Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/core/utils/error_utils.dart | 73 +++++++++++++++++++ .../providers/gamification_provider.dart | 7 +- .../reviews/services/review_service.dart | 16 ++-- lib/features/reviews/widgets/review_form.dart | 3 +- .../reviews/widgets/review_section.dart | 3 +- lib/screens/calendar_screen.dart | 5 +- lib/screens/contribute_screen.dart | 7 +- lib/screens/desktop_login_screen.dart | 5 +- lib/screens/home_screen.dart | 3 +- lib/screens/learn_more_screen.dart | 3 +- lib/screens/login_screen.dart | 5 +- lib/screens/profile_screen.dart | 5 +- lib/screens/search_screen.dart | 3 +- 13 files changed, 111 insertions(+), 27 deletions(-) create mode 100644 lib/core/utils/error_utils.dart diff --git a/lib/core/utils/error_utils.dart b/lib/core/utils/error_utils.dart new file mode 100644 index 0000000..da58706 --- /dev/null +++ b/lib/core/utils/error_utils.dart @@ -0,0 +1,73 @@ +// lib/core/utils/error_utils.dart + +/// Converts raw exceptions into user-friendly messages. +/// Strips technical details (hostnames, ports, stack traces, exception chains) +/// and returns a clean message safe to display in the UI. +String userFriendlyError(Object e) { + final raw = e.toString(); + + // Network / connectivity issues + if (raw.contains('SocketException') || + raw.contains('Connection refused') || + raw.contains('Connection reset') || + raw.contains('Network is unreachable') || + raw.contains('No address associated') || + raw.contains('Failed to fetch') || + raw.contains('HandshakeException') || + raw.contains('ClientException')) { + return 'Unable to connect. Please check your internet connection and try again.'; + } + + // Timeout + if (raw.contains('TimeoutException') || raw.contains('timed out')) { + return 'The request took too long. Please try again.'; + } + + // Rate limited + if (raw.contains('status 429') || raw.contains('throttled') || raw.contains('Too Many Requests')) { + return 'Too many requests. Please wait a moment and try again.'; + } + + // Auth expired / forbidden + if (raw.contains('status 401') || raw.contains('Unauthorized')) { + return 'Session expired. Please log in again.'; + } + if (raw.contains('status 403') || raw.contains('Forbidden')) { + return 'You do not have permission to perform this action.'; + } + + // Server error + if (RegExp(r'status 5\d\d').hasMatch(raw)) { + return 'Something went wrong on our end. Please try again later.'; + } + + // Not found + if (raw.contains('status 404') || raw.contains('Not Found')) { + return 'The requested resource was not found.'; + } + + // Strip Exception wrappers and nested chains for validation messages + var cleaned = raw + .replaceAll(RegExp(r'Exception:\s*'), '') + .replaceAll(RegExp(r'Failed to \w+ \w+:\s*'), '') + .replaceAll(RegExp(r'Network error:\s*'), '') + .replaceAll(RegExp(r'Request failed \(status \d+\)\s*'), '') + .trim(); + + // If the cleaned message is empty or still looks technical, use a generic fallback + if (cleaned.isEmpty || + cleaned.contains('errno') || + cleaned.contains('address =') || + cleaned.contains('port =') || + cleaned.startsWith('{') || + cleaned.startsWith('[')) { + return 'Something went wrong. Please try again.'; + } + + // Capitalize first letter + if (cleaned.isNotEmpty) { + cleaned = cleaned[0].toUpperCase() + cleaned.substring(1); + } + + return cleaned; +} diff --git a/lib/features/gamification/providers/gamification_provider.dart b/lib/features/gamification/providers/gamification_provider.dart index 5b84c3e..d3d1d21 100644 --- a/lib/features/gamification/providers/gamification_provider.dart +++ b/lib/features/gamification/providers/gamification_provider.dart @@ -1,6 +1,7 @@ // lib/features/gamification/providers/gamification_provider.dart import 'package:flutter/foundation.dart'; +import '../../../core/utils/error_utils.dart'; import '../models/gamification_models.dart'; import '../services/gamification_service.dart'; @@ -41,7 +42,7 @@ class GamificationProvider extends ChangeNotifier { shopItems = results[2] as List; achievements = results[3] as List; } catch (e) { - error = e.toString(); + error = userFriendlyError(e); } finally { isLoading = false; notifyListeners(); @@ -58,7 +59,7 @@ class GamificationProvider extends ChangeNotifier { try { leaderboard = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod); } catch (e) { - error = e.toString(); + error = userFriendlyError(e); } notifyListeners(); } @@ -73,7 +74,7 @@ class GamificationProvider extends ChangeNotifier { try { leaderboard = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period); } catch (e) { - error = e.toString(); + error = userFriendlyError(e); } notifyListeners(); } diff --git a/lib/features/reviews/services/review_service.dart b/lib/features/reviews/services/review_service.dart index 3c67d82..b86d2b5 100644 --- a/lib/features/reviews/services/review_service.dart +++ b/lib/features/reviews/services/review_service.dart @@ -53,8 +53,8 @@ class ReviewService { page: (res['page'] as num?)?.toInt() ?? page, pageSize: (res['page_size'] as num?)?.toInt() ?? pageSize, ); - } catch (e) { - throw Exception('Failed to load reviews: $e'); + } catch (_) { + rethrow; } } @@ -70,8 +70,8 @@ class ReviewService { }, requiresAuth: true, ); - } catch (e) { - throw Exception('Failed to submit review: $e'); + } catch (_) { + rethrow; } } @@ -84,8 +84,8 @@ class ReviewService { requiresAuth: true, ); return (res['helpful_count'] as num?)?.toInt() ?? 0; - } catch (e) { - throw Exception('Failed to mark review as helpful: $e'); + } catch (_) { + rethrow; } } @@ -97,8 +97,8 @@ class ReviewService { body: {'review_id': reviewId}, requiresAuth: true, ); - } catch (e) { - throw Exception('Failed to flag review: $e'); + } catch (_) { + rethrow; } } } diff --git a/lib/features/reviews/widgets/review_form.dart b/lib/features/reviews/widgets/review_form.dart index fb3f562..7c8bf19 100644 --- a/lib/features/reviews/widgets/review_form.dart +++ b/lib/features/reviews/widgets/review_form.dart @@ -1,6 +1,7 @@ // lib/features/reviews/widgets/review_form.dart import 'package:flutter/material.dart'; import '../../../core/storage/token_storage.dart'; +import '../../../core/utils/error_utils.dart'; import '../models/review_models.dart'; import 'star_rating_input.dart'; @@ -60,7 +61,7 @@ class _ReviewFormState extends State { }); } } catch (e) { - if (mounted) setState(() { _state = _FormState.idle; _error = e.toString(); }); + if (mounted) setState(() { _state = _FormState.idle; _error = userFriendlyError(e); }); } } diff --git a/lib/features/reviews/widgets/review_section.dart b/lib/features/reviews/widgets/review_section.dart index 8e962c1..9fc35d2 100644 --- a/lib/features/reviews/widgets/review_section.dart +++ b/lib/features/reviews/widgets/review_section.dart @@ -1,6 +1,7 @@ // lib/features/reviews/widgets/review_section.dart import 'package:flutter/material.dart'; import '../../../core/storage/token_storage.dart'; +import '../../../core/utils/error_utils.dart'; import '../models/review_models.dart'; import '../services/review_service.dart'; import 'review_summary.dart'; @@ -55,7 +56,7 @@ class _ReviewSectionState extends State { }); } } catch (e) { - if (mounted) setState(() { _loading = false; _error = e.toString(); }); + if (mounted) setState(() { _loading = false; _error = userFriendlyError(e); }); } } diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 054bac1..22dcba2 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -1,5 +1,6 @@ // lib/screens/calendar_screen.dart import 'package:flutter/material.dart'; +import '../core/utils/error_utils.dart'; import 'package:intl/intl.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../features/events/services/events_service.dart'; @@ -94,7 +95,7 @@ class _CalendarScreenState extends State { } } catch (e) { - if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); } finally { if (mounted) setState(() => _loadingMonth = false); } @@ -117,7 +118,7 @@ class _CalendarScreenState extends State { final events = await _service.getEventsForDate(yyyyMMdd); if (mounted) setState(() => _eventsOfDay = events); } catch (e) { - if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); } finally { if (mounted) setState(() => _loadingDay = false); } diff --git a/lib/screens/contribute_screen.dart b/lib/screens/contribute_screen.dart index 15e9033..33b1faa 100644 --- a/lib/screens/contribute_screen.dart +++ b/lib/screens/contribute_screen.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; +import '../core/utils/error_utils.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; @@ -1829,7 +1830,7 @@ class _ContributeScreenState extends State } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick images: $e'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); } } } @@ -1868,7 +1869,7 @@ class _ContributeScreenState extends State } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red)); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)), backgroundColor: Colors.red)); } } finally { if (mounted) setState(() => _submitting = false); @@ -2652,7 +2653,7 @@ class _ContributeScreenState extends State ); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Redemption failed: $e'), backgroundColor: Colors.red)); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)), backgroundColor: Colors.red)); } } } diff --git a/lib/screens/desktop_login_screen.dart b/lib/screens/desktop_login_screen.dart index 1fac1f0..803f6a6 100644 --- a/lib/screens/desktop_login_screen.dart +++ b/lib/screens/desktop_login_screen.dart @@ -1,6 +1,7 @@ // lib/screens/desktop_login_screen.dart import 'package:flutter/material.dart'; +import '../core/utils/error_utils.dart'; import '../features/auth/services/auth_service.dart'; import '../core/auth/auth_guard.dart'; import 'home_desktop_screen.dart'; @@ -101,7 +102,7 @@ class _DesktopLoginScreenState extends State with SingleTick )); } catch (e) { if (!mounted) return; - final message = e.toString().replaceAll('Exception: ', ''); + final message = userFriendlyError(e); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); setState(() => _isAnimating = false); } finally { @@ -335,7 +336,7 @@ class _DesktopRegisterScreenState extends State { Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeDesktopScreen(skipSidebarEntranceAnimation: true))); } catch (e) { if (!mounted) return; - final message = e.toString().replaceAll('Exception: ', ''); + final message = userFriendlyError(e); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); } finally { if (mounted) setState(() => _loading = false); diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 47f64f4..a5e1e91 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,6 +1,7 @@ // lib/screens/home_screen.dart import 'dart:async'; import 'dart:ui'; +import '../core/utils/error_utils.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../core/auth/auth_guard.dart'; @@ -122,7 +123,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } catch (e) { if (mounted) { setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); } } } diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart index 7f8c87d..60f5ed8 100644 --- a/lib/screens/learn_more_screen.dart +++ b/lib/screens/learn_more_screen.dart @@ -12,6 +12,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import '../features/events/models/event_models.dart'; import '../features/events/services/events_service.dart'; import '../core/auth/auth_guard.dart'; +import '../core/utils/error_utils.dart'; import '../core/constants.dart'; import '../features/reviews/widgets/review_section.dart'; @@ -108,7 +109,7 @@ class _LearnMoreScreenState extends State { _startAutoScroll(); } catch (e) { if (!mounted) return; - setState(() => _error = e.toString()); + setState(() => _error = userFriendlyError(e)); } finally { if (mounted) setState(() => _loading = false); } diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 5ef6794..fd820ea 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -1,6 +1,7 @@ // lib/screens/login_screen.dart import 'dart:ui'; +import '../core/utils/error_utils.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; @@ -106,7 +107,7 @@ class _LoginScreenState extends State { )); } catch (e) { if (!mounted) return; - final message = e.toString().replaceAll('Exception: ', ''); + final message = userFriendlyError(e); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); } finally { if (mounted) setState(() => _loading = false); @@ -606,7 +607,7 @@ class _RegisterScreenState extends State { Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen())); } catch (e) { if (!mounted) return; - final message = e.toString().replaceAll('Exception: ', ''); + final message = userFriendlyError(e); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); } finally { if (mounted) setState(() => _loading = false); diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 9161f3e..28676b4 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; +import '../core/utils/error_utils.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -211,7 +212,7 @@ class _ProfileScreenState extends State } catch (e) { if (mounted) { ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Failed to load events: $e'))); + .showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); } } finally { if (mounted) setState(() => _loadingEvents = false); @@ -259,7 +260,7 @@ class _ProfileScreenState extends State } catch (e) { debugPrint('Image pick error: $e'); ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Failed to pick image: $e'))); + .showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); } } diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 0dfc59f..4a2c7ef 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,6 +1,7 @@ // lib/screens/search_screen.dart import 'dart:ui'; import 'package:flutter/material.dart'; +import '../core/utils/error_utils.dart'; // Location packages import 'package:geolocator/geolocator.dart'; @@ -152,7 +153,7 @@ class _SearchScreenState extends State { if (mounted) Navigator.of(context).pop('Current Location'); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not determine location: $e'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); Navigator.of(context).pop('Current Location'); } } finally {