Compare commits

...

6 Commits

Author SHA1 Message Date
Robert Brennan
48de69153c Merge branch 'fix/reduce-config-api-calls' of https://github.com/all-hands-ai/openhands into fix/reduce-config-api-calls 2024-11-05 18:31:09 -05:00
Robert Brennan
3599811c34 delint 2024-11-05 18:31:07 -05:00
openhands
8c934b6c01 fix: add proper TypeScript types for GitHub data and auth cache 2024-11-05 23:26:12 +00:00
openhands
dd6817ea75 refactor: add caching for auth and GitHub user API calls 2024-11-05 23:21:52 +00:00
Robert Brennan
c266455a01 delint 2024-11-05 18:16:02 -05:00
openhands
27ddf8c60c refactor: reduce getConfig API calls by using parent route values 2024-11-05 23:12:46 +00:00
7 changed files with 175 additions and 17 deletions

View File

@@ -3,6 +3,13 @@
* @param token The GitHub token
* @returns The headers for the GitHub API
*/
/**
* Given a GitHub token, retrieves the authenticated user
* @param token The GitHub token
* @returns The authenticated user or an error response
*/
import { authCache } from "#/utils/auth-cache";
const generateGitHubAPIHeaders = (token: string) =>
({
Accept: "application/vnd.github+json",
@@ -103,14 +110,15 @@ export const retrieveAllGitHubUserRepositories = async (
return repositories;
};
/**
* Given a GitHub token, retrieves the authenticated user
* @param token The GitHub token
* @returns The authenticated user or an error response
*/
export const retrieveGitHubUser = async (
token: string,
): Promise<GitHubUser | GitHubErrorReponse> => {
// Check cache first
const cachedUser = authCache.getGithubUser(token);
if (cachedUser !== undefined) {
return cachedUser;
}
const response = await fetch("https://api.github.com/user", {
headers: generateGitHubAPIHeaders(token),
});
@@ -124,6 +132,8 @@ export const retrieveGitHubUser = async (
avatar_url: data.avatar_url,
};
// Cache the successful response
authCache.setGithubUser(token, user);
return user;
}
@@ -133,6 +143,8 @@ export const retrieveGitHubUser = async (
status: response.status,
};
// Cache the error response too
authCache.setGithubUser(token, error);
return error;
};

View File

@@ -16,22 +16,13 @@ import { retrieveAllGitHubUserRepositories } from "#/api/github";
import store from "#/store";
import { setInitialQuery } from "#/state/initial-query-slice";
import { clientLoader as rootClientLoader } from "#/routes/_oh";
import OpenHands from "#/api/open-hands";
import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url";
import { GitHubRepositoriesSuggestionBox } from "#/components/github-repositories-suggestion-box";
export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
let isSaas = false;
let githubClientId: string | null = null;
try {
const config = await OpenHands.getConfig();
isSaas = config.APP_MODE === "saas";
githubClientId = config.GITHUB_CLIENT_ID;
} catch (error) {
isSaas = false;
githubClientId = null;
}
// Get config values from parent route
const isSaas = window.__APP_MODE__ === "saas";
const githubClientId = window.__GITHUB_CLIENT_ID__;
const ghToken = localStorage.getItem("ghToken");
const token = localStorage.getItem("token");

View File

@@ -28,6 +28,7 @@ import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import NewProjectIcon from "#/assets/new-project.svg?react";
import DocsIcon from "#/assets/docs.svg?react";
import { userIsAuthenticated } from "#/utils/user-is-authenticated";
import { authCache } from "#/utils/auth-cache";
import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url";
import { WaitlistModal } from "#/components/waitlist-modal";
import { AnalyticsConsentFormModal } from "#/components/analytics-consent-form-modal";
@@ -35,6 +36,7 @@ import { setCurrentAgentState } from "#/state/agentSlice";
import AgentState from "#/types/AgentState";
export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
console.log('client loader');
try {
const config = await OpenHands.getConfig();
window.__APP_MODE__ = config.APP_MODE;
@@ -49,6 +51,17 @@ export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
const analyticsConsent = localStorage.getItem("analytics-consent");
const userConsents = analyticsConsent === "true";
// Store current tokens to detect changes
const prevToken = (window as any).__PREV_TOKEN__;
const prevGhToken = (window as any).__PREV_GH_TOKEN__;
// Clear cache if tokens changed
if (token !== prevToken || ghToken !== prevGhToken) {
(window as any).__PREV_TOKEN__ = token;
(window as any).__PREV_GH_TOKEN__ = ghToken;
authCache.clear();
}
if (!userConsents) {
posthog.opt_out_capturing();
} else {

View File

@@ -0,0 +1,41 @@
export interface GitHubUser {
id: number;
login: string;
avatar_url: string;
}
export interface GitHubErrorReponse {
message: string;
documentation_url: string;
status: number;
}
export interface GitHubRepository {
id: number;
name: string;
full_name: string;
private: boolean;
html_url: string;
description: string | null;
fork: boolean;
created_at: string;
updated_at: string;
pushed_at: string;
git_url: string;
ssh_url: string;
clone_url: string;
default_branch: string;
}
export interface GitHubCommit {
sha: string;
commit: {
author: {
name: string;
email: string;
date: string;
};
message: string;
};
html_url: string;
}

8
frontend/src/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
declare global {
interface Window {
__APP_MODE__: "saas" | "oss";
__GITHUB_CLIENT_ID__: string | null;
__PREV_TOKEN__?: string | null;
__PREV_GH_TOKEN__?: string | null;
}
}

View File

@@ -0,0 +1,81 @@
import type { GitHubUser, GitHubErrorReponse } from "#/types/github";
interface CacheEntry<T> {
value: T;
timestamp: number;
token: string;
}
type GitHubUserResponse = GitHubUser | GitHubErrorReponse;
class AuthCache {
private static instance: AuthCache;
private cache: {
isAuthed?: CacheEntry<boolean>;
githubUser?: CacheEntry<GitHubUserResponse>;
} = {};
private constructor() {}
static getInstance(): AuthCache {
if (!AuthCache.instance) {
AuthCache.instance = new AuthCache();
}
return AuthCache.instance;
}
private isExpired<T>(entry: CacheEntry<T>, maxAge: number): boolean {
return Date.now() - entry.timestamp > maxAge;
}
private tokenChanged<T>(entry: CacheEntry<T>, currentToken: string): boolean {
return entry.token !== currentToken;
}
getAuthStatus(token: string): boolean | undefined {
const entry = this.cache.isAuthed;
if (
!entry ||
this.isExpired(entry, 60000) ||
this.tokenChanged(entry, token)
) {
return undefined;
}
return entry.value;
}
setAuthStatus(token: string, value: boolean): void {
this.cache.isAuthed = {
value,
timestamp: Date.now(),
token,
};
}
getGithubUser(token: string): GitHubUserResponse | undefined {
const entry = this.cache.githubUser;
if (
!entry ||
this.isExpired(entry, 300000) ||
this.tokenChanged(entry, token)
) {
return undefined;
}
return entry.value;
}
setGithubUser(token: string, value: GitHubUserResponse): void {
this.cache.githubUser = {
value,
timestamp: Date.now(),
token,
};
}
clear(): void {
this.cache = {};
}
}
export const authCache = AuthCache.getInstance();

View File

@@ -1,12 +1,24 @@
import OpenHands from "#/api/open-hands";
import { authCache } from "./auth-cache";
export const userIsAuthenticated = async () => {
if (window.__APP_MODE__ === "oss") return true;
const token = localStorage.getItem("token");
if (!token) return false;
// Check cache first
const cachedStatus = authCache.getAuthStatus(token);
if (cachedStatus !== undefined) {
return cachedStatus;
}
try {
await OpenHands.authenticate();
authCache.setAuthStatus(token, true);
return true;
} catch (error) {
authCache.setAuthStatus(token, false);
return false;
}
};