import { fetchWithAuth } from "@/lib"; import { OAuthProvider } from "@/types"; import { ResponseUtils, TokenReissueManager, TokenUtils, cookieStorage, tokenStorage } from "@/utils"; import Constants from "expo-constants"; import { Platform } from "react-native"; import { AuthResponse, LoginParams, NativeAppleLoginParams } from "./auth.types"; const isWeb = Platform.OS === "web"; export class AuthService { private static readonly API_BASE_URL = Constants.expoConfig?.extra?.apiBaseUrl as string; private static readonly CONTENT_TYPE_JSON = "application/json"; private static readonly DEFAULT_HEADERS = { "Content-Type": AuthService.CONTENT_TYPE_JSON, "Ramble-Client-Platform": Platform.OS, }; static async ensureAuthenticated(): Promise<{ authenticated: boolean; error?: unknown; }> { try { const [accessToken, refreshToken] = await Promise.all([ tokenStorage.getAccessToken(), cookieStorage.getRefreshToken(), ]); if (!accessToken || (Platform.OS !== "web" && !refreshToken)) { return { authenticated: false }; } if (!TokenUtils.isTokenExpired(accessToken)) { return { authenticated: true }; } const newToken = await TokenReissueManager.reissueToken(); if (newToken) { return { authenticated: true }; } return { authenticated: false }; } catch (e) { console.error("ensureAuthenticated error:", e); return { authenticated: false, error: e }; } } static async login( params: LoginParams, provider: OAuthProvider, ): Promise { try { const response = await fetch(`${AuthService.API_BASE_URL}/oauth/authorize/${provider.toLowerCase()}`, { method: "POST", ...(isWeb ? { credentials: "include" as RequestCredentials } : {}), headers: AuthService.DEFAULT_HEADERS, body: JSON.stringify(params), }); if (!response.ok) { await ResponseUtils.handleApiError(response, `${provider} 로그인`); } const tokens = TokenUtils.extractTokensFromHeaders(response.headers); await Promise.all([ tokenStorage.setAccessToken(tokens.accessToken), cookieStorage.setRefreshToken(tokens.cookieHeader), ]); return { success: true }; } catch (error) { await Promise.all([ tokenStorage.removeAccessToken(), cookieStorage.removeRefreshToken(), ]); return { success: false, error: error instanceof Error ? error.message : `${provider} 로그인에 실패했습니다`, }; } } static async loginWithNativeApple(params: NativeAppleLoginParams): Promise { try { const response = await fetch(`${AuthService.API_BASE_URL}/oauth/authorize/apple/native`, { method: "POST", ...(isWeb ? { credentials: "include" as RequestCredentials } : {}), headers: AuthService.DEFAULT_HEADERS, body: JSON.stringify(params), }); if (!response.ok) { await ResponseUtils.handleApiError(response, "Apple Native 로그인"); } const tokens = TokenUtils.extractTokensFromHeaders(response.headers); await Promise.all([ tokenStorage.setAccessToken(tokens.accessToken), cookieStorage.setRefreshToken(tokens.cookieHeader), ]); return { success: true }; } catch (error) { await Promise.all([ tokenStorage.removeAccessToken(), cookieStorage.removeRefreshToken(), ]); return { success: false, error: error instanceof Error ? error.message : "Apple Native 로그인에 실패했습니다" }; } } static async logout(): Promise { try { await fetchWithAuth("/auth/logout", { method: "POST", ...(isWeb ? { credentials: "include" as RequestCredentials } : {}), }); return { success: true }; } catch (error) { console.error("로그아웃 실패:", error); return { success: false, error: error instanceof Error ? error.message : "로그아웃에 실패했습니다" }; } finally { // API 호출 실패 시에도 로컬 토큰을 삭제하여 클라이언트 측 로그아웃 보장 await Promise.all([ tokenStorage.removeAccessToken(), cookieStorage.removeRefreshToken(), ]); } } static async withdraw(): Promise { try { const response = await fetchWithAuth("/auth/withdraw", { method: "POST", ...(isWeb ? { credentials: "include" as RequestCredentials } : {}), }); if (!response.ok) { await ResponseUtils.handleApiError(response, "회원 탈퇴"); } await Promise.all([ tokenStorage.removeAccessToken(), cookieStorage.removeRefreshToken(), ]); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "탈퇴에 실패했습니다" }; } } }