Reapply "ask for google client id"

This reverts commit efa91f8627.
This commit is contained in:
Ramnique Singh 2026-02-03 23:01:23 +05:30
parent fbdf9cd834
commit 3cfbfa7c34
12 changed files with 281 additions and 33 deletions

View file

@ -306,7 +306,7 @@ export function setupIpcHandlers() {
return runsCore.listRuns(args.cursor);
},
'oauth:connect': async (_event, args) => {
return await connectProvider(args.provider);
return await connectProvider(args.provider, args.clientId);
},
'oauth:disconnect': async (_event, args) => {
return await disconnectProvider(args.provider);

View file

@ -4,6 +4,12 @@ import { createAuthServer } from './auth-server.js';
import * as oauthClient from '@x/core/dist/auth/oauth-client.js';
import type { Configuration } from '@x/core/dist/auth/oauth-client.js';
import { getProviderConfig, getAvailableProviders } from '@x/core/dist/auth/providers.js';
import {
clearProviderClientIdOverride,
getProviderClientIdOverride,
hasProviderClientIdOverride,
setProviderClientIdOverride,
} from '@x/core/dist/auth/provider-client-id.js';
import container from '@x/core/dist/di/container.js';
import { IOAuthRepo } from '@x/core/dist/auth/repo.js';
import { IClientRegistrationRepo } from '@x/core/dist/auth/client-repo.js';
@ -76,14 +82,25 @@ function getClientRegistrationRepo(): IClientRegistrationRepo {
*/
async function getProviderConfiguration(provider: string): Promise<Configuration> {
const config = getProviderConfig(provider);
const resolveClientId = (): string => {
const override = getProviderClientIdOverride(provider);
if (override) {
return override;
}
if (config.client.mode === 'static' && config.client.clientId) {
return config.client.clientId;
}
throw new Error(`${provider} client ID not configured. Please provide a client ID.`);
};
if (config.discovery.mode === 'issuer') {
if (config.client.mode === 'static') {
// Discover endpoints, use static client ID
console.log(`[OAuth] ${provider}: Discovery from issuer with static client ID`);
const clientId = resolveClientId();
return await oauthClient.discoverConfiguration(
config.discovery.issuer,
config.client.clientId
clientId
);
} else {
// DCR mode - check for existing registration or register new
@ -120,10 +137,11 @@ async function getProviderConfiguration(provider: string): Promise<Configuration
}
console.log(`[OAuth] ${provider}: Using static endpoints (no discovery)`);
const clientId = resolveClientId();
return oauthClient.createStaticConfiguration(
config.discovery.authorizationEndpoint,
config.discovery.tokenEndpoint,
config.client.clientId,
clientId,
config.discovery.revocationEndpoint
);
}
@ -132,7 +150,7 @@ async function getProviderConfiguration(provider: string): Promise<Configuration
/**
* Initiate OAuth flow for a provider
*/
export async function connectProvider(provider: string): Promise<{ success: boolean; error?: string }> {
export async function connectProvider(provider: string, clientId?: string): Promise<{ success: boolean; error?: string }> {
try {
console.log(`[OAuth] Starting connection flow for ${provider}...`);
@ -142,6 +160,14 @@ export async function connectProvider(provider: string): Promise<{ success: bool
const oauthRepo = getOAuthRepo();
const providerConfig = getProviderConfig(provider);
if (provider === 'google') {
const trimmedClientId = clientId?.trim();
if (!trimmedClientId) {
return { success: false, error: 'Google client ID is required to connect.' };
}
setProviderClientIdOverride(provider, trimmedClientId);
}
// Get or create OAuth configuration
const config = await getProviderConfiguration(provider);
@ -256,6 +282,9 @@ export async function disconnectProvider(provider: string): Promise<{ success: b
try {
const oauthRepo = getOAuthRepo();
await oauthRepo.clearTokens(provider);
if (provider === 'google') {
clearProviderClientIdOverride(provider);
}
return { success: true };
} catch (error) {
console.error('OAuth disconnect failed:', error);
@ -269,6 +298,9 @@ export async function disconnectProvider(provider: string): Promise<{ success: b
export async function isConnected(provider: string): Promise<{ isConnected: boolean }> {
try {
const oauthRepo = getOAuthRepo();
if (provider === 'google' && !hasProviderClientIdOverride(provider)) {
return { isConnected: false };
}
const connected = await oauthRepo.isConnected(provider);
return { isConnected: connected };
} catch (error) {
@ -325,7 +357,10 @@ export async function getConnectedProviders(): Promise<{ providers: string[] }>
try {
const oauthRepo = getOAuthRepo();
const providers = await oauthRepo.getConnectedProviders();
return { providers };
const filteredProviders = providers.filter((provider) =>
provider === 'google' ? hasProviderClientIdOverride(provider) : true
);
return { providers: filteredProviders };
} catch (error) {
console.error('Get connected providers failed:', error);
return { providers: [] };

View file

@ -18,6 +18,8 @@ import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { Separator } from "@/components/ui/separator"
import { ComposioApiKeyModal } from "@/components/composio-api-key-modal"
import { GoogleClientIdModal } from "@/components/google-client-id-modal"
import { getGoogleClientId, setGoogleClientId, clearGoogleClientId } from "@/lib/google-client-id-store"
import { toast } from "sonner"
interface ProviderState {
@ -36,6 +38,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
const [providers, setProviders] = useState<string[]>([])
const [providersLoading, setProvidersLoading] = useState(true)
const [providerStates, setProviderStates] = useState<Record<string, ProviderState>>({})
const [googleClientIdOpen, setGoogleClientIdOpen] = useState(false)
// Granola state
const [granolaEnabled, setGranolaEnabled] = useState(false)
@ -266,15 +269,14 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
return cleanup
}, [])
// Connect to a provider
const handleConnect = useCallback(async (provider: string) => {
const startConnect = useCallback(async (provider: string, clientId?: string) => {
setProviderStates(prev => ({
...prev,
[provider]: { ...prev[provider], isConnecting: true }
}))
try {
const result = await window.ipc.invoke('oauth:connect', { provider })
const result = await window.ipc.invoke('oauth:connect', { provider, clientId })
if (result.success) {
// OAuth flow started - keep isConnecting state, wait for event
@ -297,6 +299,27 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
}
}, [])
// Connect to a provider
const handleConnect = useCallback(async (provider: string) => {
if (provider === 'google') {
const existingClientId = getGoogleClientId()
if (!existingClientId) {
setGoogleClientIdOpen(true)
return
}
await startConnect(provider, existingClientId)
return
}
await startConnect(provider)
}, [startConnect])
const handleGoogleClientIdSubmit = useCallback((clientId: string) => {
setGoogleClientId(clientId)
setGoogleClientIdOpen(false)
startConnect('google', clientId)
}, [startConnect])
// Disconnect from a provider
const handleDisconnect = useCallback(async (provider: string) => {
setProviderStates(prev => ({
@ -308,6 +331,9 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
const result = await window.ipc.invoke('oauth:disconnect', { provider })
if (result.success) {
if (provider === 'google') {
clearGoogleClientId()
}
const displayName = provider === 'fireflies-ai' ? 'Fireflies' : provider.charAt(0).toUpperCase() + provider.slice(1)
toast.success(`Disconnected from ${displayName}`)
setProviderStates(prev => ({
@ -395,6 +421,12 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
return (
<>
<GoogleClientIdModal
open={googleClientIdOpen}
onOpenChange={setGoogleClientIdOpen}
onSubmit={handleGoogleClientIdSubmit}
isSubmitting={providerStates.google?.isConnecting ?? false}
/>
<Popover open={open} onOpenChange={setOpen}>
{tooltip ? (
<Tooltip open={open ? false : undefined}>

View file

@ -0,0 +1,85 @@
"use client"
import { useEffect, useState } from "react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
interface GoogleClientIdModalProps {
open: boolean
onOpenChange: (open: boolean) => void
onSubmit: (clientId: string) => void
isSubmitting?: boolean
}
export function GoogleClientIdModal({
open,
onOpenChange,
onSubmit,
isSubmitting = false,
}: GoogleClientIdModalProps) {
const [clientId, setClientId] = useState("")
useEffect(() => {
if (!open) {
setClientId("")
}
}, [open])
const trimmedClientId = clientId.trim()
const isValid = trimmedClientId.length > 0
const handleSubmit = () => {
if (!isValid || isSubmitting) return
onSubmit(trimmedClientId)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Enter Google Client ID</DialogTitle>
<DialogDescription>
This app does not store the client ID. You will be prompted each session.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground" htmlFor="google-client-id">
Client ID
</label>
<Input
id="google-client-id"
placeholder="xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
value={clientId}
onChange={(event) => setClientId(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault()
handleSubmit()
}
}}
autoFocus
/>
</div>
<div className="mt-4 flex justify-end gap-2">
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!isValid || isSubmitting}>
Continue
</Button>
</div>
</DialogContent>
</Dialog>
)
}

View file

@ -15,6 +15,8 @@ import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { cn } from "@/lib/utils"
import { ComposioApiKeyModal } from "@/components/composio-api-key-modal"
import { GoogleClientIdModal } from "@/components/google-client-id-modal"
import { getGoogleClientId, setGoogleClientId } from "@/lib/google-client-id-store"
import { toast } from "sonner"
interface ProviderState {
@ -37,6 +39,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
const [providers, setProviders] = useState<string[]>([])
const [providersLoading, setProvidersLoading] = useState(true)
const [providerStates, setProviderStates] = useState<Record<string, ProviderState>>({})
const [googleClientIdOpen, setGoogleClientIdOpen] = useState(false)
// Granola state
const [granolaEnabled, setGranolaEnabled] = useState(false)
@ -245,15 +248,14 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
return cleanup
}, [])
// Connect to a provider
const handleConnect = useCallback(async (provider: string) => {
const startConnect = useCallback(async (provider: string, clientId?: string) => {
setProviderStates(prev => ({
...prev,
[provider]: { ...prev[provider], isConnecting: true }
}))
try {
const result = await window.ipc.invoke('oauth:connect', { provider })
const result = await window.ipc.invoke('oauth:connect', { provider, clientId })
if (!result.success) {
toast.error(result.error || `Failed to connect to ${provider}`)
@ -272,6 +274,27 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
}
}, [])
// Connect to a provider
const handleConnect = useCallback(async (provider: string) => {
if (provider === 'google') {
const existingClientId = getGoogleClientId()
if (!existingClientId) {
setGoogleClientIdOpen(true)
return
}
await startConnect(provider, existingClientId)
return
}
await startConnect(provider)
}, [startConnect])
const handleGoogleClientIdSubmit = useCallback((clientId: string) => {
setGoogleClientId(clientId)
setGoogleClientIdOpen(false)
startConnect('google', clientId)
}, [startConnect])
const handleNext = () => {
if (currentStep < 2) {
setCurrentStep((prev) => (prev + 1) as Step)
@ -574,6 +597,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
return (
<>
<GoogleClientIdModal
open={googleClientIdOpen}
onOpenChange={setGoogleClientIdOpen}
onSubmit={handleGoogleClientIdSubmit}
isSubmitting={providerStates.google?.isConnecting ?? false}
/>
<ComposioApiKeyModal
open={composioApiKeyOpen}
onOpenChange={setComposioApiKeyOpen}

View file

@ -50,10 +50,10 @@ export function useOAuth(provider: string) {
return cleanup;
}, [provider, checkConnection]);
const connect = useCallback(async () => {
const connect = useCallback(async (clientId?: string) => {
try {
setIsConnecting(true);
const result = await window.ipc.invoke('oauth:connect', { provider });
const result = await window.ipc.invoke('oauth:connect', { provider, clientId });
if (result.success) {
// OAuth flow started - keep isConnecting state, wait for event
// Event listener will handle the actual completion

View file

@ -0,0 +1,17 @@
let googleClientId: string | null = null;
export function getGoogleClientId(): string | null {
return googleClientId;
}
export function setGoogleClientId(clientId: string): void {
const trimmed = clientId.trim();
if (!trimmed) {
return;
}
googleClientId = trimmed;
}
export function clearGoogleClientId(): void {
googleClientId = null;
}