diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index bec54af..012828c 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -3,12 +3,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/enums_and_consts/consts.dart'; import 'package:flux/core/utils/app_message.dart'; +import 'package:flux/features/master_data/staff/data/staff_repository.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; part 'auth_state.dart'; class AuthCubit extends Cubit { final _supabase = GetIt.instance(); + final _staffRepository = GetIt.instance(); AuthCubit() : super(const AuthState()); @@ -95,10 +97,7 @@ class AuthCubit extends Cubit { ); return; } - await _supabase.auth.resetPasswordForEmail( - email, - redirectTo: resetPasswordUrl, - ); + await _staffRepository.resetPassword(email); emit( state.copyWith( status: AuthStatus.pwResetSent, diff --git a/lib/features/auth/ui/auth_screen.dart b/lib/features/auth/ui/auth_screen.dart index 3d699c1..8c8c979 100644 --- a/lib/features/auth/ui/auth_screen.dart +++ b/lib/features/auth/ui/auth_screen.dart @@ -128,6 +128,30 @@ class _AuthScreenState extends State { _submit(), // Se lo supporti nel tuo widget custom ), + // Link "Dimenticato password?" solo in Login mode + if (state.isLoginMode) ...[ + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: isLoading + ? null + : () => context + .read() + .requestPasswordReset( + _emailController.text.trim(), + ), + child: Text( + context.l10n.authScreenForgotPassword, + style: TextStyle( + color: context.accent, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + const SizedBox(height: 40), // --- BOTTONE PRINCIPALE --- diff --git a/lib/features/auth/ui/set_password_screen.dart b/lib/features/auth/ui/set_password_screen.dart index 67d630b..5adf0e0 100644 --- a/lib/features/auth/ui/set_password_screen.dart +++ b/lib/features/auth/ui/set_password_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flux/main.dart'; +import 'package:go_router/go_router.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class SetPasswordScreen extends StatefulWidget { @@ -63,7 +64,7 @@ class _SetPasswordScreenState extends State { setState(() => _isSessionReady = true); } } catch (e) { - print("Errore ripristino manuale sessione: $e"); + debugPrint("Errore ripristino manuale sessione: $e"); } } @@ -98,6 +99,7 @@ class _SetPasswordScreenState extends State { backgroundColor: Colors.green, ), ); + context.go('/'); } } on AuthException catch (e) { setState(() { diff --git a/lib/features/master_data/staff/data/staff_repository.dart b/lib/features/master_data/staff/data/staff_repository.dart index b495d3f..6067f72 100644 --- a/lib/features/master_data/staff/data/staff_repository.dart +++ b/lib/features/master_data/staff/data/staff_repository.dart @@ -105,12 +105,15 @@ class StaffRepository { } Future resetPassword(String email) async { - //TODO modificare per fare il reset della password tramite edge function, così da poter personalizzare l'email e il link di reset try { - await _supabase.auth.resetPasswordForEmail( - email, - redirectTo: resetPasswordUrl, + final response = await Supabase.instance.client.functions.invoke( + 'reset_password', + body: {'email': email.trim()}, ); + + if (response.status != 200) { + throw Exception(response.data['error'] ?? "Errore sconosciuto"); + } } catch (e) { throw Exception("Errore nell'invio del link: $e"); } diff --git a/lib/features/master_data/staff/ui/staff_screen.dart b/lib/features/master_data/staff/ui/staff_screen.dart index 886450e..0e25e60 100644 --- a/lib/features/master_data/staff/ui/staff_screen.dart +++ b/lib/features/master_data/staff/ui/staff_screen.dart @@ -7,6 +7,7 @@ import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:get_it/get_it.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; class StaffScreen extends StatefulWidget { const StaffScreen({super.key}); @@ -193,9 +194,9 @@ class _StaffScreenState extends State { if (!member.hasJoined) { context.read().inviteStaffMember( member: member, - selectedStoreIds: - member.assignedStores?.map((s) => s.id!).toList() ?? - [], + selectedStoreIds: member.assignedStores + .map((s) => s.id!) + .toList(), ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -206,13 +207,6 @@ class _StaffScreenState extends State { ); } else { context.read().resetPassword(member.email!); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Operazione richiesta, controlla l\'email!', - ), - ), - ); } } }, diff --git a/lib/main.dart b/lib/main.dart index 2296470..bd684ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,3 @@ -import 'dart:developer'; import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; @@ -55,9 +54,6 @@ void main() async { final initialUri = Uri.base; if (initialUri.fragment.contains('access_token=')) { initialRecoveryFragment = initialUri.fragment; - log( - "Ninja attivato: Token catturato in cassaforte! $initialRecoveryFragment", - ); } await dotenv.load(fileName: ".env"); diff --git a/pubspec.yaml b/pubspec.yaml index 69c54de..402b1da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flux description: "Gestione attività negozio di telefonia" publish_to: 'none' -version: 1.1.11+29 +version: 1.1.12+30 environment: sdk: ^3.11.3 diff --git a/supabase/functions/reset-password/index.ts b/supabase/functions/reset-password/index.ts new file mode 100644 index 0000000..a3ba0a5 --- /dev/null +++ b/supabase/functions/reset-password/index.ts @@ -0,0 +1,129 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts" +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.7.1' + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + +serve(async (req) => { + // 1. GESTIONE CORS + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + // 2. ESTRAZIONE EMAIL + const { email } = await req.json() + if (!email) { + throw new Error("Devi fornire un indirizzo email.") + } + + // 3. INIZIALIZZAZIONE CLIENT ADMIN + const supabaseAdmin = createClient( + Deno.env.get('SUPABASE_URL') ?? '', + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' + ) + + // 4. RECUPERO NOME UTENTE (Opzionale, ma fa molto pro per l'email) + const { data: staffData } = await supabaseAdmin + .from('staff_members') + .select('name') + .eq('email', email) + .single() + + const userName = staffData?.name || "Socio" + + // 5. GENERAZIONE DEL LINK DI RECOVERY + // Usiamo lo stesso identico URL di redirect che abbiamo blindato prima! + const { data: linkData, error: linkError } = await supabaseAdmin.auth.admin.generateLink({ + type: 'recovery', + email: email, + options: { + redirectTo: 'https://flux.catelli.it/set-password' + } + }) + + if (linkError) { + // Se l'utente non esiste su Auth, Supabase lancia un errore. + // Per sicurezza (evitare l'enumerazione delle email), restituiamo comunque un 200 finto + // oppure intercettiamo l'errore per il debug. + console.error("Errore generazione link Supabase:", linkError) + throw new Error("Se l'email è registrata, riceverai a breve le istruzioni.") + } + + const secureLink = linkData.properties.action_link + + // 6. IL POSTINO RESEND + const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY') + if (!RESEND_API_KEY) throw new Error("Chiave API di Resend non configurata.") + + const emailHtml = ` +
+ +
+ FLUX Logo +
+ +

+ Ripristino Password +

+ +

+ Ciao ${userName}, +

+ +

+ Abbiamo ricevuto una richiesta per reimpostare la password del tuo account su FLUX. Clicca sul pulsante qui sotto per sceglierne una nuova. +

+ + + +
+

+ Se non hai richiesto tu il ripristino, puoi ignorare tranquillamente questa email. Il link è strettamente personale e valido per un solo utilizzo.

+ ${secureLink} +

+
+
+ ` + + // Qui puoi cambiare l'indirizzo mittente se vuoi separare gli inviti dal supporto tecnico + const resendResponse = await fetch('https://api.resend.com/emails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${RESEND_API_KEY}`, + }, + body: JSON.stringify({ + from: 'FLUX Support ', // Modificato per sembrare più istituzionale + to: [email], + subject: 'Recupero Password FLUX', + html: emailHtml, + }), + }) + + if (!resendResponse.ok) { + const resendError = await resendResponse.text() + console.error("Resend ha fallito:", resendError) + throw new Error("Errore durante l'invio dell'email.") + } + + // 7. TUTTO OK + return new Response( + JSON.stringify({ success: true, message: "Email inviata con successo." }), + { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ) + + } catch (error: any) { + console.error("CRASH FUNZIONE RESET:", error.message || error); + return new Response( + JSON.stringify({ error: error.message || "Errore generico del server" }), + { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ) + } +}) \ No newline at end of file