259 lines
8.6 KiB
Dart
259 lines
8.6 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 생성하여 로그인
|
|
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<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;
|
|
}
|