app/lib/authService.ts (206 lines of code) (raw):

// Authentication service for API key management export interface ApiCredentials { openaiApiKey: string; huggingfaceToken: string; githubToken?: string; hfUserInfo: HFUserInfo; githubUserInfo?: GitHubUserInfo; } export interface AuthStatus { isAuthenticated: boolean; hasOpenAI: boolean; hasHuggingFace: boolean; hasGitHub?: boolean; expiresAt?: Date; hfUserInfo?: HFUserInfo; githubUserInfo?: GitHubUserInfo; } export interface HFUserInfo { username: string; fullName: string; avatarUrl: string; } export interface GitHubUserInfo { username: string; name?: string; email?: string; avatar_url?: string; } export class AuthService { private static readonly API_ENDPOINT = "/api/auth"; private static readonly CONFIG_ENDPOINT = "/api/auth/config"; // Check if user is authenticated by calling backend static async getAuthStatus(): Promise<AuthStatus> { try { const response = await fetch(this.API_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "include", // Include cookies in request body: JSON.stringify({ action: "verify" }), }); if (!response.ok) { throw new Error(`Auth verification failed: ${response.status}`); } const result = await response.json(); return { isAuthenticated: result.isAuthenticated || false, hasOpenAI: result.hasOpenAI || false, hasHuggingFace: result.hasHuggingFace || false, hasGitHub: result.hasGitHub || false, expiresAt: result.expiresAt ? new Date(result.expiresAt) : undefined, hfUserInfo: result.hfUserInfo, githubUserInfo: result.githubUserInfo, }; } catch (error) { console.error("Failed to get auth status:", error); return { isAuthenticated: false, hasOpenAI: false, hasHuggingFace: false, hasGitHub: false, hfUserInfo: undefined, githubUserInfo: undefined, }; } } // Authenticate via backend action static async authenticate(credentials: { openaiApiKey?: string; huggingfaceToken?: string; }): Promise<{ success: boolean; error?: string; hfUserInfo?: HFUserInfo }> { try { // Validate credentials format on client side first if (!this.validateCredentials(credentials)) { return { success: false, error: "Invalid credentials format" }; } const response = await fetch(this.API_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "include", // Include cookies in request body: JSON.stringify({ action: "authenticate", credentials, }), }); const result = await response.json(); if (!response.ok) { return { success: false, error: result.error || `Authentication failed: ${response.status}`, }; } return { success: true, hfUserInfo: result.hfUserInfo, }; } catch (error) { console.error("Authentication request failed:", error); return { success: false, error: "Network error during authentication", }; } } // Get stored credentials and verify the caller with existing auth path static async getCredentials(): Promise<ApiCredentials | null> { if (typeof window === "undefined") return null; // Use the existing verify endpoint to check authentication try { // First, get auth status which verifies the cookie is valid const status = await this.getAuthStatus(); if (!status.isAuthenticated) { console.log("AuthService.getCredentials: Not authenticated"); return null; } const userInfo = status.hfUserInfo || null; // Since we're getting credentials through the auth status, // we don't need to decrypt anything here - just return the user info return { openaiApiKey: "", // OpenAI key is handled as a regular secret now huggingfaceToken: "", // Token is verified server-side, not needed client-side hfUserInfo: userInfo || { username: "", fullName: "", avatarUrl: "", }, }; } catch (error) { console.error("Failed to get credentials:", error); return null; } } // Check if OAuth2 is available static async isOAuth2Available(): Promise<boolean> { try { const response = await fetch(this.CONFIG_ENDPOINT); if (response.ok) { const config = await response.json(); return config.oauth2Enabled || false; } } catch (error) { console.error("Failed to check OAuth2 availability:", error); } return false; } // Check if GitHub OAuth2 is available static async isGitHubOAuth2Available(): Promise<boolean> { try { const response = await fetch(this.CONFIG_ENDPOINT); if (response.ok) { const config = await response.json(); return config.githubOAuth2Enabled || false; } } catch (error) { console.error("Failed to check GitHub OAuth2 availability:", error); } return false; } // Start OAuth2 login flow (HuggingFace) static startOAuth2Login(returnTo?: string): void { const params = new URLSearchParams(); if (returnTo) { params.set("returnTo", returnTo); } window.location.href = `/api/auth/login?${params.toString()}`; } // Start GitHub OAuth2 login flow static startGitHubOAuth2Login(returnTo?: string): void { const params = new URLSearchParams(); if (returnTo) { params.set("returnTo", returnTo); } // Open GitHub OAuth in a popup window const popup = window.open( `/api/auth/github/login?${params.toString()}`, "github-oauth", "width=600,height=700,scrollbars=yes,resizable=yes" ); // Simply refresh the page in 3 seconds setTimeout(() => { window.location.reload(); }, 3000); // if (!popup) { // alert("Popup blocked. Please allow popups for GitHub OAuth."); // return; // } // // Listen for the popup to close // const checkClosed = setInterval(() => { // if (popup?.closed) { // clearInterval(checkClosed); // console.log("GitHub OAuth popup closed, refreshing auth status..."); // // Wait a moment for cookies to be set, then refresh the page // setTimeout(() => { // window.location.reload(); // }, 500); // } // }, 1000); // // Handle popup messages for error cases // const handleMessage = (event: MessageEvent) => { // if (event.origin !== window.location.origin) return; // if (event.data.type === "GITHUB_OAUTH2_ERROR") { // popup.close(); // clearInterval(checkClosed); // alert("GitHub authentication failed: " + event.data.error); // } // }; // window.addEventListener("message", handleMessage); // // Cleanup message listener when popup closes // const originalInterval = checkClosed; // const cleanupInterval = setInterval(() => { // if (popup?.closed) { // window.removeEventListener("message", handleMessage); // clearInterval(cleanupInterval); // } // }, 1000); } // Logout via backend action static async logout(): Promise<boolean> { try { const response = await fetch(this.API_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "include", // Include cookies in request body: JSON.stringify({ action: "logout" }), }); return response.ok; } catch (error) { console.error("Logout request failed:", error); return false; } } // Validate credential format (client-side validation) private static validateCredentials(credentials: { openaiApiKey?: string; huggingfaceToken?: string; }): boolean { // OpenAI API key validation (starts with sk-) - optional if ( credentials.openaiApiKey && !credentials.openaiApiKey.startsWith("sk-") ) { return false; } // HuggingFace token validation (starts with hf_) - required if ( !credentials.huggingfaceToken || !credentials.huggingfaceToken.startsWith("hf_") ) { return false; } return true; } // // Extend session by re-authenticating with existing credentials // static async extendSession(): Promise<boolean> { // const credentials = this.getCredentials(); // if (!credentials) return false; // const result = await this.authenticate({ // openaiApiKey: credentials.openaiApiKey, // huggingfaceToken: credentials.huggingfaceToken, // }); // return result.success; // } // // Convenience method for checking auth status synchronously (from cookie) // static getAuthStatusSync(): AuthStatus { // if (typeof window === "undefined") { // return { // isAuthenticated: false, // hasOpenAI: false, // hasHuggingFace: false, // }; // } // const cookie = document.cookie; // if (!cookie) { // return { // isAuthenticated: false, // hasOpenAI: false, // hasHuggingFace: false, // }; // } // try { // const data = JSON.parse(atob(cookie)); // const now = new Date(); // const expiresAt = new Date(data.expiresAt); // if (now > expiresAt) { // return { // isAuthenticated: false, // hasOpenAI: false, // hasHuggingFace: false, // }; // } // return { // isAuthenticated: true, // hasOpenAI: !!data.hasOpenAI, // hasHuggingFace: !!data.hasHuggingFace, // expiresAt, // }; // } catch (error) { // console.error("Failed to parse auth cookie:", error); // return { // isAuthenticated: false, // hasOpenAI: false, // hasHuggingFace: false, // }; // } // } }