import 'package:firebase_auth/firebase_auth.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter/material.dart'; import '../main.dart'; import '../screens/welcome_screen.dart'; import '../utils/log_manager.dart'; class AuthService { final FirebaseAuth _auth = FirebaseAuth.instance; // Google Sign-In 설정 (Backend와 통신하기 위해 serverClientId 지정) final GoogleSignIn _googleSignIn = GoogleSignIn( serverClientId: '379988243470-g6490l8gucc3ljras93i28c3l4qlroi4.apps.googleusercontent.com', ); final Dio _dio = Dio(); final FlutterSecureStorage _storage = const FlutterSecureStorage(); // Backend Base URL final String _baseUrl = 'http://10.0.2.2:3000/auth'; AuthService() { // Dio Options 설정 (Timeout 추가) _dio.options.connectTimeout = const Duration(seconds: 5); // 5초 연결 타임아웃 _dio.options.receiveTimeout = const Duration(seconds: 5); // 5초 응답 타임아웃 // Dio Interceptor 설정 _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { // 모든 요청 헤더에 Access Token 추가 final accessToken = await _storage.read(key: 'accessToken'); if (accessToken != null) { options.headers['Authorization'] = 'Bearer $accessToken'; } return handler.next(options); }, onError: (DioException error, handler) async { // 401 에러 발생 시 (Access Token 만료) if (error.response?.statusCode == 401) { String msg1 = '[Auth] Access Token expired. Attempting refresh...'; print(msg1); LogManager().addLog(msg1); // LOG final isRefreshed = await _refreshToken(); if (isRefreshed) { // 토큰 갱신 성공 -> 원래 요청 재시도 final newAccessToken = await _storage.read(key: 'accessToken'); // 헤더 업데이트 error.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken'; // 재요청 try { final response = await _dio.fetch(error.requestOptions); return handler.resolve(response); } catch (e) { return handler.reject(error); } } else { // 토큰 갱신 실패 (리프레시 토큰도 만료됨) -> 로그아웃 & 화면 이동 String msg2 = '[Auth] Refresh Token expired. Logging out...'; print(msg2); LogManager().addLog(msg2); // LOG await signOut(); // Force Navigation to Welcome Screen navigatorKey.currentState?.pushAndRemoveUntil( MaterialPageRoute(builder: (context) => const WelcomeScreen()), (route) => false, ); } } else { // Log other errors LogManager().addLog( '[DioError] ${error.message} (Status: ${error.response?.statusCode})', ); } return handler.next(error); }, ), ); } // 토큰 갱신 로직 Future _refreshToken() async { try { final refreshToken = await _storage.read(key: 'refreshToken'); if (refreshToken == null) return false; final response = await _dio.post( '$_baseUrl/refresh', data: {'refreshToken': refreshToken}, ); if (response.statusCode == 200 && response.data['success'] == true) { final newAccessToken = response.data['accessToken']; await _storage.write(key: 'accessToken', value: newAccessToken); String msg = '[Auth] Token refreshed successfully.'; print(msg); LogManager().addLog(msg); return true; } return false; } catch (e) { String msg = '[Auth] Token refresh failed: $e'; print(msg); LogManager().addLog(msg); return false; } } // 구글 로그인 (Check or Login) Future?> signInWithGoogle() async { try { LogManager().addLog('[DEBUG] Google Sign-In: Starting signIn()'); final GoogleSignInAccount? googleUser = await _googleSignIn.signIn(); if (googleUser == null) { LogManager().addLog('[DEBUG] Google Sign-In: User canceled'); return null; } final GoogleSignInAuthentication googleAuth = await googleUser.authentication; if (googleAuth.idToken != null) { try { // 1. 서버에 토큰 전송 (Login Check) final response = await _dio.post( '$_baseUrl/google', data: {'idToken': googleAuth.idToken}, ); if (response.statusCode == 200 && response.data['success'] == true) { final isNewUser = response.data['isNewUser'] ?? false; if (isNewUser) { // 신규 유저: 토큰 저장하지 않고 idToken 반환 (약관 동의 화면으로 전달) return { 'isNewUser': true, 'idToken': googleAuth.idToken, 'email': response.data['email'], 'nickname': response.data['nickname'], }; } else { // 기존 유저: 토큰 저장 및 로그인 처리 final accessToken = response.data['accessToken']; final refreshToken = response.data['refreshToken']; await _storage.write(key: 'accessToken', value: accessToken); await _storage.write(key: 'refreshToken', value: refreshToken); // Firebase 로그인 (선택 사항, 필요하다면 유지) final OAuthCredential credential = GoogleAuthProvider.credential( accessToken: googleAuth.accessToken, idToken: googleAuth.idToken, ); await _auth.signInWithCredential(credential); return {'isNewUser': false}; } } } catch (e) { String msg = '[ERROR] Backend Auth API Error: $e'; print(msg); LogManager().addLog(msg); } } return null; } catch (e) { String msg = '[ERROR] Google Sign-In Error: $e'; print(msg); LogManager().addLog(msg); return null; } } // 구글 회원가입 (Register - 약관 동의 후 호출) Future registerWithGoogle(String idToken) async { try { final response = await _dio.post( '$_baseUrl/google/register', data: {'idToken': idToken}, ); if (response.statusCode == 200 && response.data['success'] == true) { final accessToken = response.data['accessToken']; final refreshToken = response.data['refreshToken']; await _storage.write(key: 'accessToken', value: accessToken); await _storage.write(key: 'refreshToken', value: refreshToken); // Firebase 로그인 처리 // idToken으로 Credential 생성하여 로그인 final OAuthCredential credential = GoogleAuthProvider.credential( idToken: idToken, accessToken: null, // accessToken은 필수가 아님 (idToken만으로 가능) ); await _auth.signInWithCredential(credential); return true; } return false; } catch (e) { LogManager().addLog('[Auth] Register Failed: $e'); return false; } } // 로그아웃 Future signOut() async { await _googleSignIn.signOut(); await _auth.signOut(); await _storage.deleteAll(); // 토큰 삭제 print('[DEBUG] User signed out and tokens cleared.'); } // Access Token 가져오기 Future getAccessToken() async { return await _storage.read(key: 'accessToken'); } // 유저 정보 가져오기 Future?> getUserInfo() async { try { final response = await _dio.get('$_baseUrl/me'); if (response.statusCode == 200 && response.data['success'] == true) { return response.data['user']; } return null; } catch (e) { LogManager().addLog('[Auth] Get User Info Failed: $e'); return null; } } // 회원 탈퇴 Future withdrawAccount() async { try { final response = await _dio.delete('$_baseUrl/withdraw'); if (response.statusCode == 200 && response.data['success'] == true) { await _googleSignIn.signOut(); await _auth.signOut(); await _storage.deleteAll(); return true; } return false; } catch (e) { LogManager().addLog('[Auth] Withdraw Account Failed: $e'); return false; } } Dio get dio => _dio; }