From 049f839a62a42d9771ea3e814dd894ee53c95381 Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:31:41 +0700 Subject: [PATCH] refactor(frontend): move auth APIs to a dedicated service handler (#10932) --- .../__tests__/routes/git-settings.test.tsx | 3 +- .../src/api/auth-service/auth-service.api.ts | 52 +++++++++++++++++++ frontend/src/api/auth-service/auth.types.ts | 8 +++ frontend/src/api/open-hands.ts | 38 -------------- frontend/src/api/open-hands.types.ts | 29 ++++------- frontend/src/hooks/mutation/use-logout.ts | 4 +- frontend/src/hooks/query/use-is-authed.ts | 4 +- frontend/src/mocks/handlers.ts | 2 +- 8 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 frontend/src/api/auth-service/auth-service.api.ts create mode 100644 frontend/src/api/auth-service/auth.types.ts diff --git a/frontend/__tests__/routes/git-settings.test.tsx b/frontend/__tests__/routes/git-settings.test.tsx index d84113fa6f..e09ad9cc7e 100644 --- a/frontend/__tests__/routes/git-settings.test.tsx +++ b/frontend/__tests__/routes/git-settings.test.tsx @@ -7,6 +7,7 @@ import i18next from "i18next"; import { I18nextProvider } from "react-i18next"; import GitSettingsScreen from "#/routes/git-settings"; import OpenHands from "#/api/open-hands"; +import AuthService from "#/api/auth-service/auth-service.api"; import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; import { GetConfigResponse } from "#/api/open-hands.types"; import * as ToastHandlers from "#/utils/custom-toast-handlers"; @@ -392,7 +393,7 @@ describe("Form submission", () => { it("should call logout when pressing the disconnect tokens button", async () => { const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); - const logoutSpy = vi.spyOn(OpenHands, "logout"); + const logoutSpy = vi.spyOn(AuthService, "logout"); const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG); diff --git a/frontend/src/api/auth-service/auth-service.api.ts b/frontend/src/api/auth-service/auth-service.api.ts new file mode 100644 index 0000000000..f06782d0a9 --- /dev/null +++ b/frontend/src/api/auth-service/auth-service.api.ts @@ -0,0 +1,52 @@ +import { openHands } from "../open-hands-axios"; +import { AuthenticateResponse, GitHubAccessTokenResponse } from "./auth.types"; +import { GetConfigResponse } from "../open-hands.types"; + +/** + * Authentication service for handling all authentication-related API calls + */ +class AuthService { + /** + * Authenticate with GitHub token + * @param appMode The application mode (saas or oss) + * @returns Response with authentication status and user info if successful + */ + static async authenticate( + appMode: GetConfigResponse["APP_MODE"], + ): Promise { + if (appMode === "oss") return true; + + // Just make the request, if it succeeds (no exception thrown), return true + await openHands.post("/api/authenticate"); + return true; + } + + /** + * Get GitHub access token from Keycloak callback + * @param code Code provided by GitHub + * @returns GitHub access token + */ + static async getGitHubAccessToken( + code: string, + ): Promise { + const { data } = await openHands.post( + "/api/keycloak/callback", + { + code, + }, + ); + return data; + } + + /** + * Logout user from the application + * @param appMode The application mode (saas or oss) + */ + static async logout(appMode: GetConfigResponse["APP_MODE"]): Promise { + const endpoint = + appMode === "saas" ? "/api/logout" : "/api/unset-provider-tokens"; + await openHands.post(endpoint); + } +} + +export default AuthService; diff --git a/frontend/src/api/auth-service/auth.types.ts b/frontend/src/api/auth-service/auth.types.ts new file mode 100644 index 0000000000..d69bcb708e --- /dev/null +++ b/frontend/src/api/auth-service/auth.types.ts @@ -0,0 +1,8 @@ +export interface AuthenticateResponse { + message?: string; + error?: string; +} + +export interface GitHubAccessTokenResponse { + access_token: string; +} diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index 1b98431917..dc009b7e9f 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -2,10 +2,8 @@ import { AxiosHeaders } from "axios"; import { Feedback, FeedbackResponse, - GitHubAccessTokenResponse, GetConfigResponse, GetVSCodeUrlResponse, - AuthenticateResponse, Conversation, ResultSet, GetTrajectoryResponse, @@ -210,20 +208,6 @@ class OpenHands { return data; } - /** - * Authenticate with GitHub token - * @returns Response with authentication status and user info if successful - */ - static async authenticate( - appMode: GetConfigResponse["APP_MODE"], - ): Promise { - if (appMode === "oss") return true; - - // Just make the request, if it succeeds (no exception thrown), return true - await openHands.post("/api/authenticate"); - return true; - } - /** * Get the blob of the workspace zip * @returns Blob of the workspace zip @@ -249,22 +233,6 @@ class OpenHands { return Object.keys(response.data.hosts); } - /** - * @param code Code provided by GitHub - * @returns GitHub access token - */ - static async getGitHubAccessToken( - code: string, - ): Promise { - const { data } = await openHands.post( - "/api/keycloak/callback", - { - code, - }, - ); - return data; - } - /** * Get the VSCode URL * @returns VSCode URL @@ -487,12 +455,6 @@ class OpenHands { return data; } - static async logout(appMode: GetConfigResponse["APP_MODE"]): Promise { - const endpoint = - appMode === "saas" ? "/api/logout" : "/api/unset-provider-tokens"; - await openHands.post(endpoint); - } - static async getGitChanges(conversationId: string): Promise { const url = `${this.getConversationUrl(conversationId)}/git/changes`; const { data } = await openHands.get(url, { diff --git a/frontend/src/api/open-hands.types.ts b/frontend/src/api/open-hands.types.ts index 0cc650a670..c3fac3f8da 100644 --- a/frontend/src/api/open-hands.types.ts +++ b/frontend/src/api/open-hands.types.ts @@ -26,10 +26,6 @@ export interface FeedbackResponse { body: FeedbackBodyResponse; } -export interface GitHubAccessTokenResponse { - access_token: string; -} - export interface AuthenticationResponse { message: string; login?: string; // Only present when allow list is enabled @@ -44,6 +40,16 @@ export interface Feedback { trajectory: unknown[]; } +export interface GetVSCodeUrlResponse { + vscode_url: string | null; + error?: string; +} + +export interface GetTrajectoryResponse { + trajectory: unknown[] | null; + error?: string; +} + export interface GetConfigResponse { APP_MODE: "saas" | "oss"; APP_SLUG?: string; @@ -63,21 +69,6 @@ export interface GetConfigResponse { }; } -export interface GetVSCodeUrlResponse { - vscode_url: string | null; - error?: string; -} - -export interface GetTrajectoryResponse { - trajectory: unknown[] | null; - error?: string; -} - -export interface AuthenticateResponse { - message?: string; - error?: string; -} - export interface RepositorySelection { selected_repository: string | null; selected_branch: string | null; diff --git a/frontend/src/hooks/mutation/use-logout.ts b/frontend/src/hooks/mutation/use-logout.ts index 283250a2ff..d3fa98f28e 100644 --- a/frontend/src/hooks/mutation/use-logout.ts +++ b/frontend/src/hooks/mutation/use-logout.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import posthog from "posthog-js"; -import OpenHands from "#/api/open-hands"; +import AuthService from "#/api/auth-service/auth-service.api"; import { useConfig } from "../query/use-config"; import { clearLoginData } from "#/utils/local-storage"; @@ -9,7 +9,7 @@ export const useLogout = () => { const { data: config } = useConfig(); return useMutation({ - mutationFn: () => OpenHands.logout(config?.APP_MODE ?? "oss"), + mutationFn: () => AuthService.logout(config?.APP_MODE ?? "oss"), onSuccess: async () => { queryClient.removeQueries({ queryKey: ["tasks"] }); queryClient.removeQueries({ queryKey: ["settings"] }); diff --git a/frontend/src/hooks/query/use-is-authed.ts b/frontend/src/hooks/query/use-is-authed.ts index b1ad0c7cd8..f8aa3aa6ca 100644 --- a/frontend/src/hooks/query/use-is-authed.ts +++ b/frontend/src/hooks/query/use-is-authed.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import axios, { AxiosError } from "axios"; -import OpenHands from "#/api/open-hands"; +import AuthService from "#/api/auth-service/auth-service.api"; import { useConfig } from "./use-config"; import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page"; @@ -15,7 +15,7 @@ export const useIsAuthed = () => { queryFn: async () => { try { // If in OSS mode or authentication succeeds, return true - await OpenHands.authenticate(appMode!); + await AuthService.authenticate(appMode!); return true; } catch (error) { // If it's a 401 error, return false (not authenticated) diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 12153c4d45..3b97b0d318 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -1,8 +1,8 @@ import { delay, http, HttpResponse } from "msw"; import { - GetConfigResponse, Conversation, ResultSet, + GetConfigResponse, } from "#/api/open-hands.types"; import { DEFAULT_SETTINGS } from "#/services/settings"; import { STRIPE_BILLING_HANDLERS } from "./billing-handlers";