rup-project/app/lib/services/auth_service.dart

257 lines
8.8 KiB
Dart

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<bool> _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<Map<String, dynamic>?> 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<bool> 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 생성 가능하지만 access token이 없으므로 생략하거나
// 이미 signInWithGoogle에서 받아온 credential을 재사용할 수 있으면 좋음.
// 하지만 여기서는 백엔드 세션이 중요하므로 일단 백엔드 토큰만 저장.
// (Firebase Auth와 Custom Backend Auth를 혼용 중이라 복잡함.
// 일단 백엔드 로직이 메인이므로 백엔드 토큰 처리에 집중)
return true;
}
return false;
} catch (e) {
LogManager().addLog('[Auth] Register Failed: $e');
return false;
}
}
// 로그아웃
Future<void> signOut() async {
await _googleSignIn.signOut();
await _auth.signOut();
await _storage.deleteAll(); // 토큰 삭제
print('[DEBUG] User signed out and tokens cleared.');
}
// Access Token 가져오기
Future<String?> getAccessToken() async {
return await _storage.read(key: 'accessToken');
}
// 유저 정보 가져오기
Future<Map<String, dynamic>?> 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<bool> 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;
}