Ramble-FE / services / auth / auth.service.ts
auth.service.ts
Raw
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<AuthResponse> {
        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<AuthResponse> {
        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<AuthResponse> {
        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<AuthResponse> {
        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 : "탈퇴에 실패했습니다"
            };
        }
    }
}