fix(stability): remove auth/context force unwraps and add guarded null-safe paths

This commit is contained in:
Lukas Müllner 2026-03-05 14:39:00 +01:00
parent f41708c662
commit 947992e8b9
5 changed files with 60 additions and 15 deletions

View file

@ -68,8 +68,12 @@ void showGlobalSnackBar(String message, {bool isError = false}) {
/// Show a dialog safely through the global navigator key. /// Show a dialog safely through the global navigator key.
Future<T?> showGlobalDialog<T>({required WidgetBuilder builder}) { Future<T?> showGlobalDialog<T>({required WidgetBuilder builder}) {
final context = navigatorKey.currentContext;
if (context == null) {
return Future<T?>.value(null);
}
return showDialog<T>( return showDialog<T>(
context: navigatorKey.currentContext!, context: context,
builder: builder, builder: builder,
); );
} }
@ -201,8 +205,14 @@ class _AuthGateState extends State<AuthGate> {
} }
Future<void> _showResetPasswordDialog() async { Future<void> _showResetPasswordDialog() async {
final context = navigatorKey.currentContext;
if (context == null) {
_isInPasswordRecoveryFlow = false;
return;
}
await showDialog<void>( await showDialog<void>(
context: navigatorKey.currentContext!, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (_) => const _ResetPasswordDialog(), builder: (_) => const _ResetPasswordDialog(),
); );

View file

@ -483,6 +483,11 @@ class GarageScreenState extends State<GarageScreen> {
.update({'collection_id': targetId}) .update({'collection_id': targetId})
.inFilter('id', _selectedIds.toList()); .inFilter('id', _selectedIds.toList());
} else { } else {
final userId = supabase.auth.currentUser?.id;
if (userId == null) {
throw Exception('You must be signed in to copy cars.');
}
final sourceCars = _cars final sourceCars = _cars
.where((car) => _selectedIds.contains(car['id'] as int)) .where((car) => _selectedIds.contains(car['id'] as int))
.toList(growable: false); .toList(growable: false);
@ -492,7 +497,7 @@ class GarageScreenState extends State<GarageScreen> {
final imagePath = car['user_image_url'] as String?; final imagePath = car['user_image_url'] as String?;
return <String, dynamic>{ return <String, dynamic>{
'hw_id': car['hw_id'] as String, 'hw_id': car['hw_id'] as String,
'user_id': supabase.auth.currentUser!.id, 'user_id': userId,
'collection_id': targetId, 'collection_id': targetId,
if (notes != null && notes.trim().isNotEmpty) 'notes': notes, if (notes != null && notes.trim().isNotEmpty) 'notes': notes,
if (imagePath != null && imagePath.isNotEmpty) if (imagePath != null && imagePath.isNotEmpty)
@ -1028,11 +1033,16 @@ class GarageScreenState extends State<GarageScreen> {
.update({'collection_id': targetId}) .update({'collection_id': targetId})
.eq('id', car['id']); .eq('id', car['id']);
} else { } else {
final userId = supabase.auth.currentUser?.id;
if (userId == null) {
throw Exception('You must be signed in to copy cars.');
}
final notes = car['notes'] as String?; final notes = car['notes'] as String?;
final imagePath = car['user_image_url'] as String?; final imagePath = car['user_image_url'] as String?;
await supabase.from('hotwheels').insert({ await supabase.from('hotwheels').insert({
'hw_id': car['hw_id'] as String, 'hw_id': car['hw_id'] as String,
'user_id': supabase.auth.currentUser!.id, 'user_id': userId,
'collection_id': targetId, 'collection_id': targetId,
if (notes != null && notes.trim().isNotEmpty) 'notes': notes, if (notes != null && notes.trim().isNotEmpty) 'notes': notes,
if (imagePath != null && imagePath.isNotEmpty) if (imagePath != null && imagePath.isNotEmpty)

View file

@ -419,9 +419,14 @@ class ScanTabState extends State<ScanTab> {
String hwId, { String hwId, {
String? notes, String? notes,
}) async { }) async {
final userId = supabase.auth.currentUser?.id;
if (userId == null) {
throw Exception('You must be signed in to add cars.');
}
await supabase.from('hotwheels').insert({ await supabase.from('hotwheels').insert({
'hw_id': hwId, 'hw_id': hwId,
'user_id': supabase.auth.currentUser!.id, 'user_id': userId,
'collection_id': collectionId, 'collection_id': collectionId,
if (notes != null && notes.trim().isNotEmpty) 'notes': notes.trim(), if (notes != null && notes.trim().isNotEmpty) 'notes': notes.trim(),
}); });
@ -433,6 +438,11 @@ class ScanTabState extends State<ScanTab> {
String? series, String? series,
int? year, int? year,
}) async { }) async {
final userId = supabase.auth.currentUser?.id;
if (userId == null) {
throw Exception('You must be signed in to create catalog entries.');
}
final cleanedSeries = series?.trim(); final cleanedSeries = series?.trim();
final payload = <String, dynamic>{ final payload = <String, dynamic>{
'hw_id': hwId, 'hw_id': hwId,
@ -445,12 +455,15 @@ class ScanTabState extends State<ScanTab> {
await supabase.from('car_votes').insert({ await supabase.from('car_votes').insert({
'hw_id': hwId, 'hw_id': hwId,
'user_id': supabase.auth.currentUser!.id, 'user_id': userId,
}); });
} }
Future<void> _ensureValidationVote(String hwId) async { Future<void> _ensureValidationVote(String hwId) async {
final userId = supabase.auth.currentUser!.id; final userId = supabase.auth.currentUser?.id;
if (userId == null) {
throw Exception('You must be signed in to validate entries.');
}
final existingVote = await supabase final existingVote = await supabase
.from('car_votes') .from('car_votes')
.select('id') .select('id')

View file

@ -53,6 +53,14 @@ class CollectionMember {
class CollectionService { class CollectionService {
CollectionService._(); CollectionService._();
static String _requireUserId() {
final userId = supabase.auth.currentUser?.id;
if (userId == null) {
throw Exception('You must be signed in to perform this action.');
}
return userId;
}
/// Ensures the current user has at least one collection membership. /// Ensures the current user has at least one collection membership.
/// Creates a default collection on first login. /// Creates a default collection on first login.
static Future<String?> ensureDefaultCollection() async { static Future<String?> ensureDefaultCollection() async {
@ -80,7 +88,7 @@ class CollectionService {
/// Fetch all collections the current user is a member of, /// Fetch all collections the current user is a member of,
/// including item count and member count. /// including item count and member count.
static Future<List<Collection>> getMyCollections() async { static Future<List<Collection>> getMyCollections() async {
final userId = supabase.auth.currentUser!.id; final userId = _requireUserId();
// Get memberships with collection data. // Get memberships with collection data.
final memberships = await supabase final memberships = await supabase
@ -249,7 +257,7 @@ class CollectionService {
required String name, required String name,
String? description, String? description,
}) async { }) async {
final userId = supabase.auth.currentUser!.id; final userId = _requireUserId();
final row = await supabase final row = await supabase
.from('collections') .from('collections')
@ -287,7 +295,7 @@ class CollectionService {
required String name, required String name,
String? description, String? description,
}) async { }) async {
final userId = supabase.auth.currentUser!.id; final userId = _requireUserId();
final collection = await supabase final collection = await supabase
.from('collections') .from('collections')
@ -311,7 +319,7 @@ class CollectionService {
/// Delete a collection. Owner only. Cascade deletes members & items. /// Delete a collection. Owner only. Cascade deletes members & items.
static Future<void> delete(String collectionId) async { static Future<void> delete(String collectionId) async {
final userId = supabase.auth.currentUser!.id; final userId = _requireUserId();
final collection = await supabase final collection = await supabase
.from('collections') .from('collections')
@ -362,7 +370,7 @@ class CollectionService {
throw Exception('Unsupported role "$role".'); throw Exception('Unsupported role "$role".');
} }
final currentUserId = supabase.auth.currentUser!.id; final currentUserId = _requireUserId();
final collection = await supabase final collection = await supabase
.from('collections') .from('collections')
@ -428,7 +436,7 @@ class CollectionService {
required String collectionId, required String collectionId,
required String memberUserId, required String memberUserId,
}) async { }) async {
final currentUserId = supabase.auth.currentUser!.id; final currentUserId = _requireUserId();
final collection = await supabase final collection = await supabase
.from('collections') .from('collections')
@ -501,7 +509,7 @@ class CollectionService {
/// Leave a collection (for non-owners). /// Leave a collection (for non-owners).
static Future<void> leave(String collectionId) async { static Future<void> leave(String collectionId) async {
final userId = supabase.auth.currentUser!.id; final userId = _requireUserId();
final membership = await supabase final membership = await supabase
.from('collection_members') .from('collection_members')

View file

@ -31,7 +31,11 @@ class StorageService {
await deleteCarImage(oldPath); await deleteCarImage(oldPath);
} }
final userId = supabase.auth.currentUser!.id; final user = supabase.auth.currentUser;
if (user == null) {
throw Exception('You must be signed in to upload images.');
}
final userId = user.id;
final path = '$userId/$entryId.jpg'; final path = '$userId/$entryId.jpg';
final compressed = await _compressImage(file); final compressed = await _compressImage(file);