Merge pull request #393 from rowboatlabs/dev

fix google oauth
This commit is contained in:
Ramnique Singh 2026-02-21 23:05:09 +05:30 committed by GitHub
commit cd8e3586bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 44 additions and 100 deletions

View file

@ -84,7 +84,7 @@ async function getProviderConfiguration(provider: string, clientIdOverride?: str
return clientIdOverride; return clientIdOverride;
} }
const oauthRepo = getOAuthRepo(); const oauthRepo = getOAuthRepo();
const clientId = await oauthRepo.getClientId(provider); const { clientId } = await oauthRepo.read(provider);
if (clientId) { if (clientId) {
return clientId; return clientId;
} }
@ -179,9 +179,9 @@ export async function connectProvider(provider: string, clientId?: string): Prom
// Build authorization URL // Build authorization URL
const authUrl = oauthClient.buildAuthorizationUrl(config, { const authUrl = oauthClient.buildAuthorizationUrl(config, {
redirectUri: REDIRECT_URI, redirect_uri: REDIRECT_URI,
scope: scopes.join(' '), scope: scopes.join(' '),
codeChallenge, code_challenge: codeChallenge,
state, state,
}); });
@ -212,11 +212,11 @@ export async function connectProvider(provider: string, clientId?: string): Prom
// Save tokens // Save tokens
console.log(`[OAuth] Token exchange successful for ${provider}`); console.log(`[OAuth] Token exchange successful for ${provider}`);
await oauthRepo.saveTokens(provider, tokens); await oauthRepo.upsert(provider, { tokens });
if (provider === 'google' && clientId) { if (provider === 'google' && clientId) {
await oauthRepo.setClientId(provider, clientId); await oauthRepo.upsert(provider, { clientId });
} }
await oauthRepo.clearError(provider); await oauthRepo.upsert(provider, { error: null });
// Trigger immediate sync for relevant providers // Trigger immediate sync for relevant providers
if (provider === 'google') { if (provider === 'google') {
@ -281,7 +281,7 @@ export async function connectProvider(provider: string, clientId?: string): Prom
export async function disconnectProvider(provider: string): Promise<{ success: boolean }> { export async function disconnectProvider(provider: string): Promise<{ success: boolean }> {
try { try {
const oauthRepo = getOAuthRepo(); const oauthRepo = getOAuthRepo();
await oauthRepo.clearTokens(provider); await oauthRepo.delete(provider);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('OAuth disconnect failed:', error); console.error('OAuth disconnect failed:', error);
@ -297,7 +297,7 @@ export async function getAccessToken(provider: string): Promise<string | null> {
try { try {
const oauthRepo = getOAuthRepo(); const oauthRepo = getOAuthRepo();
let tokens = await oauthRepo.getTokens(provider); const { tokens } = await oauthRepo.read(provider);
if (!tokens) { if (!tokens) {
return null; return null;
} }
@ -306,7 +306,7 @@ export async function getAccessToken(provider: string): Promise<string | null> {
if (oauthClient.isTokenExpired(tokens)) { if (oauthClient.isTokenExpired(tokens)) {
if (!tokens.refresh_token) { if (!tokens.refresh_token) {
// No refresh token, need to reconnect // No refresh token, need to reconnect
await oauthRepo.setError(provider, 'Missing refresh token. Please reconnect.'); await oauthRepo.upsert(provider, { error: 'Missing refresh token. Please reconnect.' });
return null; return null;
} }
@ -316,11 +316,11 @@ export async function getAccessToken(provider: string): Promise<string | null> {
// Refresh token, preserving existing scopes // Refresh token, preserving existing scopes
const existingScopes = tokens.scopes; const existingScopes = tokens.scopes;
tokens = await oauthClient.refreshTokens(config, tokens.refresh_token, existingScopes); const refreshedTokens = await oauthClient.refreshTokens(config, tokens.refresh_token, existingScopes);
await oauthRepo.saveTokens(provider, tokens); await oauthRepo.upsert(provider, { tokens });
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Token refresh failed'; const message = error instanceof Error ? error.message : 'Token refresh failed';
await oauthRepo.setError(provider, message); await oauthRepo.upsert(provider, { error: message });
console.error('Token refresh failed:', error); console.error('Token refresh failed:', error);
return null; return null;
} }

View file

@ -152,19 +152,11 @@ export function generateState(): string {
*/ */
export function buildAuthorizationUrl( export function buildAuthorizationUrl(
config: client.Configuration, config: client.Configuration,
params: { params: Record<string, string>
redirectUri: string;
scope: string;
codeChallenge: string;
state: string;
}
): URL { ): URL {
return client.buildAuthorizationUrl(config, { return client.buildAuthorizationUrl(config, {
redirect_uri: params.redirectUri,
scope: params.scope,
code_challenge: params.codeChallenge,
code_challenge_method: 'S256', code_challenge_method: 'S256',
state: params.state, ...params,
}); });
} }
@ -208,6 +200,11 @@ export async function refreshTokens(
tokens.scopes = existingScopes; tokens.scopes = existingScopes;
} }
// Preserve existing refresh token if server didn't return it
if (!tokens.refresh_token) {
tokens.refresh_token = refreshToken;
}
console.log(`[OAuth] Token refresh successful`); console.log(`[OAuth] Token refresh successful`);
return tokens; return tokens;
} }

View file

@ -5,9 +5,9 @@ import { OAuthTokens } from './types.js';
import z from 'zod'; import z from 'zod';
const ProviderConnectionSchema = z.object({ const ProviderConnectionSchema = z.object({
tokens: OAuthTokens, tokens: OAuthTokens.nullable().optional(),
clientId: z.string().optional(), clientId: z.string().nullable().optional(),
error: z.string().optional(), error: z.string().nullable().optional(),
}); });
const OAuthConfigSchema = z.object({ const OAuthConfigSchema = z.object({
@ -17,7 +17,7 @@ const OAuthConfigSchema = z.object({
const ClientFacingConfigSchema = z.record(z.string(), z.object({ const ClientFacingConfigSchema = z.record(z.string(), z.object({
connected: z.boolean(), connected: z.boolean(),
error: z.string().optional(), error: z.string().nullable().optional(),
})); }));
const LegacyOauthConfigSchema = z.record(z.string(), OAuthTokens); const LegacyOauthConfigSchema = z.record(z.string(), OAuthTokens);
@ -28,13 +28,9 @@ const DEFAULT_CONFIG: z.infer<typeof OAuthConfigSchema> = {
}; };
export interface IOAuthRepo { export interface IOAuthRepo {
getTokens(provider: string): Promise<OAuthTokens | null>; read(provider: string): Promise<z.infer<typeof ProviderConnectionSchema>>;
saveTokens(provider: string, tokens: OAuthTokens): Promise<void>; upsert(provider: string, connection: Partial<z.infer<typeof ProviderConnectionSchema>>): Promise<void>;
clearTokens(provider: string): Promise<void>; delete(provider: string): Promise<void>;
getClientId(provider: string): Promise<string | null>;
setClientId(provider: string, clientId: string): Promise<void>;
setError(provider: string, errorMessage: string): Promise<void>;
clearError(provider: string): Promise<void>;
getClientFacingConfig(): Promise<z.infer<typeof ClientFacingConfigSchema>>; getClientFacingConfig(): Promise<z.infer<typeof ClientFacingConfigSchema>>;
} }
@ -92,71 +88,22 @@ export class FSOAuthRepo implements IOAuthRepo {
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2)); await fs.writeFile(this.configPath, JSON.stringify(config, null, 2));
} }
async getTokens(provider: string): Promise<OAuthTokens | null> { async read(provider: string): Promise<z.infer<typeof ProviderConnectionSchema>> {
const config = await this.readConfig(); const config = await this.readConfig();
const tokens = config.providers[provider]?.tokens; return config.providers[provider] ?? {};
return tokens ?? null;
} }
async upsert(provider: string, connection: Partial<z.infer<typeof ProviderConnectionSchema>>): Promise<void> {
async saveTokens(provider: string, tokens: OAuthTokens): Promise<void> {
const config = await this.readConfig(); const config = await this.readConfig();
if (config.providers[provider]) { config.providers[provider] = { ...config.providers[provider] ?? {}, ...connection };
delete config.providers[provider];
}
config.providers[provider] = {
tokens,
};
await this.writeConfig(config); await this.writeConfig(config);
} }
async clearTokens(provider: string): Promise<void> { async delete(provider: string): Promise<void> {
const config = await this.readConfig(); const config = await this.readConfig();
delete config.providers[provider]; delete config.providers[provider];
await this.writeConfig(config); await this.writeConfig(config);
} }
async getClientId(provider: string): Promise<string | null> {
const config = await this.readConfig();
const clientId = config.providers[provider]?.clientId;
return clientId ?? null;
}
async setClientId(provider: string, clientId: string): Promise<void> {
const config = await this.readConfig();
if (!config.providers[provider]) {
throw new Error(`Provider ${provider} not found`);
}
config.providers[provider].clientId = clientId;
await this.writeConfig(config);
}
async clearClientId(provider: string): Promise<void> {
const config = await this.readConfig();
if (!config.providers[provider]) {
throw new Error(`Provider ${provider} not found`);
}
delete config.providers[provider].clientId;
await this.writeConfig(config);
}
async setError(provider: string, errorMessage: string): Promise<void> {
const config = await this.readConfig();
if (!config.providers[provider]) {
throw new Error(`Provider ${provider} not found`);
}
config.providers[provider].error = errorMessage;
await this.writeConfig(config);
}
async clearError(provider: string): Promise<void> {
const config = await this.readConfig();
if (!config.providers[provider]) {
throw new Error(`Provider ${provider} not found`);
}
delete config.providers[provider].error;
await this.writeConfig(config);
}
async getClientFacingConfig(): Promise<z.infer<typeof ClientFacingConfigSchema>> { async getClientFacingConfig(): Promise<z.infer<typeof ClientFacingConfigSchema>> {
const config = await this.readConfig(); const config = await this.readConfig();
const clientFacingConfig: z.infer<typeof ClientFacingConfigSchema> = {}; const clientFacingConfig: z.infer<typeof ClientFacingConfigSchema> = {};

View file

@ -31,7 +31,7 @@ export class FirefliesClientFactory {
*/ */
static async getClient(): Promise<Client | null> { static async getClient(): Promise<Client | null> {
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo'); const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
const tokens = await oauthRepo.getTokens(this.PROVIDER_NAME); const { tokens } = await oauthRepo.read(this.PROVIDER_NAME);
if (!tokens) { if (!tokens) {
this.clearCache(); this.clearCache();
@ -49,7 +49,7 @@ export class FirefliesClientFactory {
// Token expired, try to refresh // Token expired, try to refresh
if (!tokens.refresh_token) { if (!tokens.refresh_token) {
console.log("[Fireflies] Token expired and no refresh token available."); console.log("[Fireflies] Token expired and no refresh token available.");
await oauthRepo.setError(this.PROVIDER_NAME, 'Missing refresh token. Please reconnect.'); await oauthRepo.upsert(this.PROVIDER_NAME, { error: 'Missing refresh token. Please reconnect.' });
this.clearCache(); this.clearCache();
return null; return null;
} }
@ -62,7 +62,7 @@ export class FirefliesClientFactory {
tokens.refresh_token, tokens.refresh_token,
existingScopes existingScopes
); );
await oauthRepo.saveTokens(this.PROVIDER_NAME, refreshedTokens); await oauthRepo.upsert(this.PROVIDER_NAME, { tokens: refreshedTokens });
// Update cached tokens and recreate client // Update cached tokens and recreate client
this.cache.tokens = refreshedTokens; this.cache.tokens = refreshedTokens;
@ -77,7 +77,7 @@ export class FirefliesClientFactory {
return this.cache.client; return this.cache.client;
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Failed to refresh token for Fireflies'; const message = error instanceof Error ? error.message : 'Failed to refresh token for Fireflies';
await oauthRepo.setError(this.PROVIDER_NAME, message); await oauthRepo.upsert(this.PROVIDER_NAME, { error: message });
console.error("[Fireflies] Failed to refresh token:", error); console.error("[Fireflies] Failed to refresh token:", error);
this.clearCache(); this.clearCache();
return null; return null;
@ -107,7 +107,7 @@ export class FirefliesClientFactory {
*/ */
static async hasValidCredentials(): Promise<boolean> { static async hasValidCredentials(): Promise<boolean> {
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo'); const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
const tokens = await oauthRepo.getTokens(this.PROVIDER_NAME); const { tokens } = await oauthRepo.read(this.PROVIDER_NAME);
return tokens !== null; return tokens !== null;
} }

View file

@ -27,9 +27,9 @@ export class GoogleClientFactory {
private static async resolveClientId(): Promise<string> { private static async resolveClientId(): Promise<string> {
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo'); const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
const clientId = await oauthRepo.getClientId(this.PROVIDER_NAME); const { clientId } = await oauthRepo.read(this.PROVIDER_NAME);
if (!clientId) { if (!clientId) {
await oauthRepo.setError(this.PROVIDER_NAME, 'Google client ID missing. Please reconnect.'); await oauthRepo.upsert(this.PROVIDER_NAME, { error: 'Google client ID missing. Please reconnect.' });
throw new Error('Google client ID missing. Please reconnect.'); throw new Error('Google client ID missing. Please reconnect.');
} }
return clientId; return clientId;
@ -40,7 +40,7 @@ export class GoogleClientFactory {
*/ */
static async getClient(): Promise<OAuth2Client | null> { static async getClient(): Promise<OAuth2Client | null> {
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo'); const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
const tokens = await oauthRepo.getTokens(this.PROVIDER_NAME); const { tokens } = await oauthRepo.read(this.PROVIDER_NAME);
if (!tokens) { if (!tokens) {
this.clearCache(); this.clearCache();
@ -64,7 +64,7 @@ export class GoogleClientFactory {
// Token expired, try to refresh // Token expired, try to refresh
if (!tokens.refresh_token) { if (!tokens.refresh_token) {
console.log("[OAuth] Token expired and no refresh token available for Google."); console.log("[OAuth] Token expired and no refresh token available for Google.");
await oauthRepo.setError(this.PROVIDER_NAME, 'Missing refresh token. Please reconnect.'); await oauthRepo.upsert(this.PROVIDER_NAME, { error: 'Missing refresh token. Please reconnect.' });
this.clearCache(); this.clearCache();
return null; return null;
} }
@ -77,7 +77,7 @@ export class GoogleClientFactory {
tokens.refresh_token, tokens.refresh_token,
existingScopes existingScopes
); );
await oauthRepo.saveTokens(this.PROVIDER_NAME, refreshedTokens); await oauthRepo.upsert(this.PROVIDER_NAME, { tokens: refreshedTokens });
// Update cached tokens and recreate client // Update cached tokens and recreate client
this.cache.tokens = refreshedTokens; this.cache.tokens = refreshedTokens;
@ -89,7 +89,7 @@ export class GoogleClientFactory {
return this.cache.client; return this.cache.client;
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Failed to refresh token for Google'; const message = error instanceof Error ? error.message : 'Failed to refresh token for Google';
await oauthRepo.setError(this.PROVIDER_NAME, message); await oauthRepo.upsert(this.PROVIDER_NAME, { error: message });
console.error("[OAuth] Failed to refresh token for Google:", error); console.error("[OAuth] Failed to refresh token for Google:", error);
this.clearCache(); this.clearCache();
return null; return null;
@ -116,7 +116,7 @@ export class GoogleClientFactory {
*/ */
static async hasValidCredentials(requiredScopes: string | string[]): Promise<boolean> { static async hasValidCredentials(requiredScopes: string | string[]): Promise<boolean> {
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo'); const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
const tokens = await oauthRepo.getTokens(this.PROVIDER_NAME); const { tokens } = await oauthRepo.read(this.PROVIDER_NAME);
if (!tokens) { if (!tokens) {
return false; return false;
} }

View file

@ -244,7 +244,7 @@ const ipcSchemas = {
res: z.object({ res: z.object({
config: z.record(z.string(), z.object({ config: z.record(z.string(), z.object({
connected: z.boolean(), connected: z.boolean(),
error: z.string().optional(), error: z.string().nullable().optional(),
})), })),
}), }),
}, },