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 : "탈퇴에 실패했습니다"
};
}
}
}