refactor(frontend): move auth APIs to a dedicated service handler (#10932)

This commit is contained in:
Hiep Le
2025-09-11 22:31:41 +07:00
committed by GitHub
parent 0dde758e13
commit 049f839a62
8 changed files with 77 additions and 63 deletions

View File

@@ -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);

View File

@@ -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<boolean> {
if (appMode === "oss") return true;
// Just make the request, if it succeeds (no exception thrown), return true
await openHands.post<AuthenticateResponse>("/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<GitHubAccessTokenResponse> {
const { data } = await openHands.post<GitHubAccessTokenResponse>(
"/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<void> {
const endpoint =
appMode === "saas" ? "/api/logout" : "/api/unset-provider-tokens";
await openHands.post(endpoint);
}
}
export default AuthService;

View File

@@ -0,0 +1,8 @@
export interface AuthenticateResponse {
message?: string;
error?: string;
}
export interface GitHubAccessTokenResponse {
access_token: string;
}

View File

@@ -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<boolean> {
if (appMode === "oss") return true;
// Just make the request, if it succeeds (no exception thrown), return true
await openHands.post<AuthenticateResponse>("/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<GitHubAccessTokenResponse> {
const { data } = await openHands.post<GitHubAccessTokenResponse>(
"/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<void> {
const endpoint =
appMode === "saas" ? "/api/logout" : "/api/unset-provider-tokens";
await openHands.post(endpoint);
}
static async getGitChanges(conversationId: string): Promise<GitChange[]> {
const url = `${this.getConversationUrl(conversationId)}/git/changes`;
const { data } = await openHands.get<GitChange[]>(url, {

View File

@@ -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;

View File

@@ -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"] });

View File

@@ -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)

View File

@@ -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";