diff --git a/src/providers/core.ts b/src/providers/core.ts index bf73fb9..6be828b 100644 --- a/src/providers/core.ts +++ b/src/providers/core.ts @@ -98,7 +98,7 @@ export abstract class CoreProvider { } // Authentication methods - abstract getAuthUrl(scopes: string[]): string + abstract getAuthUrl(scopes: string[], state: string): string abstract exchangeCodeForToken(code: string): Promise abstract refreshAccessToken(): Promise abstract validateAuth(): Promise diff --git a/src/providers/fortnox.ts b/src/providers/fortnox.ts index 9d9d996..c5699e6 100644 --- a/src/providers/fortnox.ts +++ b/src/providers/fortnox.ts @@ -25,14 +25,14 @@ export class FortnoxProvider extends CoreProvider { } } - getAuthUrl(scopes: string[]): string { + getAuthUrl(scopes: string[], state: string): string { // Fortnox uses a different OAuth2 flow const params = new URLSearchParams({ response_type: 'code', client_id: this.config.clientId, redirect_uri: this.config.redirectUri || '', scope: scopes.join(' '), - state: crypto.randomUUID() + state: state }) return `https://apps.fortnox.se/oauth-v1/auth?${params.toString()}` diff --git a/src/providers/quickbooks.ts b/src/providers/quickbooks.ts index 82f71ed..6a34ec2 100644 --- a/src/providers/quickbooks.ts +++ b/src/providers/quickbooks.ts @@ -26,14 +26,14 @@ export class QuickBooksProvider extends CoreProvider { } } - getAuthUrl(scopes: string[]): string { + getAuthUrl(scopes: string[], state: string): string { const params = new URLSearchParams({ client_id: this.config.clientId, scope: scopes.join(' ') || 'com.intuit.quickbooks.accounting', redirect_uri: this.config.redirectUri || '', response_type: 'code', access_type: 'offline', - state: crypto.randomUUID() + state: state }) return `${this.discoveryUrl}?${params.toString()}` diff --git a/src/providers/sage.ts b/src/providers/sage.ts index b695df0..bc5cf5b 100644 --- a/src/providers/sage.ts +++ b/src/providers/sage.ts @@ -27,13 +27,13 @@ export class SageProvider extends CoreProvider { } } - getAuthUrl(scopes: string[]): string { + getAuthUrl(scopes: string[], state: string): string { const params = new URLSearchParams({ client_id: this.config.clientId, redirect_uri: this.config.redirectUri || '', response_type: 'code', scope: scopes.join(' ') || 'full_access', - state: crypto.randomUUID() + state: state }) return `${this.authUrl}?${params.toString()}` diff --git a/src/providers/xero.ts b/src/providers/xero.ts index fa863e2..c868e7b 100644 --- a/src/providers/xero.ts +++ b/src/providers/xero.ts @@ -11,13 +11,13 @@ export class XeroProvider extends CoreProvider { } } - getAuthUrl(scopes: string[]): string { + getAuthUrl(scopes: string[], state: string): string { const params = new URLSearchParams({ response_type: 'code', client_id: this.config.clientId, redirect_uri: this.config.redirectUri || '', scope: scopes.join(' '), - state: crypto.randomUUID() + state: state, }) return `https://login.xero.com/identity/connect/authorize?${params.toString()}` diff --git a/src/routes/api.ts b/src/routes/api.ts index e34b513..ca90b8a 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -70,7 +70,12 @@ const getAuthUrlRoute = createRoute({ schema: z.object({ provider: z.enum(['xero', 'fortnox', 'quickbooks', 'sage']), scopes: z.array(z.string()), - redirectUri: z.string().url().optional() + redirectUri: z.string().url().optional(), + state: z.string().optional(), + config: z.object({ + clientId: z.string(), + clientSecret: z.string() + }) }) } } @@ -103,20 +108,20 @@ const getAuthUrlRoute = createRoute({ }) app.openapi(getAuthUrlRoute, async (c) => { - const { provider, scopes, redirectUri } = await c.req.json() + const { provider, scopes, redirectUri, state, config } = await c.req.json() const providerInstance = await providerManager.initializeProvider(provider, { - clientId: c.env.XERO_CLIENT_ID || 'placeholder', - clientSecret: c.env.XERO_CLIENT_SECRET || 'placeholder', - redirectUri: redirectUri || c.env.REDIRECT_URI || 'http://localhost:3000/callback' + clientId: config.clientId, + clientSecret: config.clientSecret, + redirectUri: redirectUri || c.env.REDIRECT_URI || 'http://localhost:3000/callback', }) - const authUrl = providerInstance.getAuthUrl(scopes) - const state = crypto.randomUUID() - + const stateParam = state || crypto.randomUUID() + const authUrl = providerInstance.getAuthUrl(scopes, stateParam) + return c.json({ authUrl, - state, + state: stateParam, provider }) }) @@ -132,7 +137,11 @@ const exchangeTokenRoute = createRoute({ schema: z.object({ provider: z.enum(['xero', 'fortnox', 'quickbooks', 'sage']), code: z.string(), - state: z.string().optional() + state: z.string().optional(), + config: z.object({ + clientId: z.string(), + clientSecret: z.string(), + }) }) } } @@ -167,11 +176,11 @@ const exchangeTokenRoute = createRoute({ }) app.openapi(exchangeTokenRoute, async (c) => { - const { provider, code } = await c.req.json() + const { provider, code, config } = await c.req.json() const providerInstance = await providerManager.initializeProvider(provider, { - clientId: c.env.XERO_CLIENT_ID || 'placeholder', - clientSecret: c.env.XERO_CLIENT_SECRET || 'placeholder' + clientId: config.clientId, + clientSecret: config.clientSecret, }) const auth = await providerInstance.exchangeCodeForToken(code)