mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 08:26:22 +02:00
commit
cd8e3586bc
6 changed files with 44 additions and 100 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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> = {};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue