mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 17:06:23 +02:00
parent
fbdf9cd834
commit
3cfbfa7c34
12 changed files with 281 additions and 33 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: [] };
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
17
apps/x/apps/renderer/src/lib/google-client-id-store.ts
Normal file
17
apps/x/apps/renderer/src/lib/google-client-id-store.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue