feat(oauth): added atlassian OAuth2 credentials

This commit is contained in:
Waleed Latif
2025-03-25 02:08:02 -07:00
parent be14c98413
commit bceab2cea3
7 changed files with 122 additions and 49 deletions
+1 -5
View File
@@ -3,7 +3,6 @@ import { eq } from 'drizzle-orm'
import { jwtDecode } from 'jwt-decode'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { OAuthService } from '@/lib/oauth'
import { db } from '@/db'
import { account, user } from '@/db/schema'
@@ -15,9 +14,6 @@ interface GoogleIdToken {
name?: string
}
// Valid OAuth providers
const VALID_PROVIDERS = ['google', 'github', 'x']
/**
* Get all OAuth connections for the current user
*/
@@ -53,7 +49,7 @@ export async function GET(request: NextRequest) {
// Extract the base provider and feature type from providerId (e.g., 'google-email' -> 'google', 'email')
const [provider, featureType = 'default'] = acc.providerId.split('-')
if (provider && VALID_PROVIDERS.includes(provider)) {
if (provider) {
// Try multiple methods to get a user-friendly display name
let displayName = ''
@@ -43,6 +43,7 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'https://www.googleapis.com/auth/userinfo.email': 'View your email address',
'https://www.googleapis.com/auth/userinfo.profile': 'View your basic profile info',
'https://www.googleapis.com/auth/spreadsheets': 'View and manage your Google Sheets',
'read:confluence-content.all': 'Read Confluence content',
repo: 'Access your repositories',
workflow: 'Manage repository workflows',
'user:email': 'Access your email address',
+20 -16
View File
@@ -20,11 +20,14 @@ export const ConfluenceBlock: BlockConfig<ConfluenceRetrieveResponse> = {
placeholder: 'Enter Confluence domain (e.g., yourcompany.atlassian.net)',
},
{
id: 'email',
title: 'Email',
type: 'short-input',
id: 'credential',
title: 'Confluence Account',
type: 'oauth-input',
layout: 'full',
placeholder: 'Enter your Atlassian email address',
provider: 'confluence',
serviceId: 'confluence',
requiredScopes: ['read:confluence-content.all', 'read:me', 'offline_access'],
placeholder: 'Select Confluence account',
},
{
id: 'pageId',
@@ -33,24 +36,25 @@ export const ConfluenceBlock: BlockConfig<ConfluenceRetrieveResponse> = {
layout: 'full',
placeholder: 'Enter the confluence page ID (e.g., 12340)',
},
{
id: 'apiKey',
title: 'OAuth Token',
type: 'short-input',
layout: 'full',
placeholder: 'Enter your Confluence OAuth token',
password: true,
connectionDroppable: false,
},
],
tools: {
access: ['confluence_retrieve'],
config: {
tool: () => 'confluence_retrieve',
params: (params) => {
const { credential, ...rest } = params
return {
accessToken: credential,
...rest,
}
},
},
},
inputs: {
apiKey: { type: 'string', required: true },
pageId: { type: 'string', required: true },
domain: { type: 'string', required: true },
email: { type: 'string', required: true },
credential: { type: 'string', required: true },
pageId: { type: 'string', required: true },
},
outputs: {
response: {
+2 -2
View File
@@ -2,6 +2,7 @@
import { AgentBlock } from './blocks/agent'
import { ApiBlock } from './blocks/api'
import { ConditionBlock } from './blocks/condition'
import { ConfluenceBlock } from './blocks/confluence'
import { GoogleDocsBlock } from './blocks/docs'
import { GoogleDriveBlock } from './blocks/drive'
import { EvaluatorBlock } from './blocks/evaluator'
@@ -30,7 +31,6 @@ import { WhatsAppBlock } from './blocks/whatsapp'
import { XBlock } from './blocks/x'
import { YouTubeBlock } from './blocks/youtube'
import { BlockConfig } from './types'
import { ConfluenceBlock } from './blocks/confluence'
// Export blocks for ease of use
export {
@@ -72,6 +72,7 @@ const blocks: Record<string, BlockConfig> = {
agent: AgentBlock,
api: ApiBlock,
condition: ConditionBlock,
confluence: ConfluenceBlock,
evaluator: EvaluatorBlock,
exa: ExaBlock,
firecrawl: FirecrawlBlock,
@@ -99,7 +100,6 @@ const blocks: Record<string, BlockConfig> = {
whatsapp: WhatsAppBlock,
x: XBlock,
youtube: YouTubeBlock,
confluence: ConfluenceBlock,
}
// Helper functions
+51 -1
View File
@@ -52,7 +52,7 @@ export const auth = betterAuth({
accountLinking: {
enabled: true,
allowDifferentEmails: true,
trustedProviders: ['google', 'github', 'email-password'],
trustedProviders: ['google', 'github', 'email-password', 'confluence'],
},
},
socialProviders: {
@@ -296,6 +296,56 @@ export const auth = betterAuth({
}
},
},
// Confluence provider
{
providerId: 'confluence',
clientId: process.env.CONFLUENCE_CLIENT_ID as string,
clientSecret: process.env.CONFLUENCE_CLIENT_SECRET as string,
authorizationUrl: 'https://auth.atlassian.com/authorize',
tokenUrl: 'https://auth.atlassian.com/oauth/token',
userInfoUrl: 'https://api.atlassian.com/me',
scopes: ['read:confluence-content.all', 'read:me', 'offline_access'],
responseType: 'code',
pkce: true,
accessType: 'offline',
prompt: 'consent',
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/confluence`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://api.atlassian.com/me', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
if (!response.ok) {
logger.error('Error fetching Confluence user info:', {
status: response.status,
statusText: response.statusText,
})
return null
}
const profile = await response.json()
const now = new Date()
return {
id: profile.account_id,
name: profile.name || profile.display_name || 'Confluence User',
email: profile.email || `${profile.account_id}@atlassian.com`,
image: profile.picture || null,
emailVerified: true, // Assume verified since it's an Atlassian account
createdAt: now,
updatedAt: now,
}
} catch (error) {
logger.error('Error in Confluence getUserInfo:', { error })
return null
}
},
},
],
}),
],
+27 -1
View File
@@ -1,5 +1,6 @@
import { ReactNode } from 'react'
import {
ConfluenceIcon,
GithubIcon,
GmailIcon,
GoogleCalendarIcon,
@@ -15,7 +16,7 @@ import { createLogger } from '@/lib/logs/console-logger'
const logger = createLogger('OAuth')
// Define the base OAuth provider type
export type OAuthProvider = 'google' | 'github' | 'x' | 'supabase' | string
export type OAuthProvider = 'google' | 'github' | 'x' | 'supabase' | 'confluence' | string
export type OAuthService =
| 'google'
| 'google-email'
@@ -25,6 +26,7 @@ export type OAuthService =
| 'github'
| 'x'
| 'supabase'
| 'confluence'
// Define the interface for OAuth provider configuration
export interface OAuthProviderConfig {
@@ -171,6 +173,23 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'supabase',
},
confluence: {
id: 'confluence',
name: 'Confluence',
icon: (props) => ConfluenceIcon(props),
services: {
confluence: {
id: 'confluence',
name: 'Confluence',
description: 'Access Confluence content and documentation.',
providerId: 'confluence',
icon: (props) => ConfluenceIcon(props),
baseProviderIcon: (props) => ConfluenceIcon(props),
scopes: ['read:confluence-content.all', 'read:me', 'offline_access'],
},
},
defaultService: 'confluence',
},
}
// Helper function to get a service by provider and service ID
@@ -219,6 +238,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]
return 'supabase'
} else if (provider === 'x') {
return 'x'
} else if (provider === 'confluence') {
return 'confluence'
}
return providerConfig.defaultService
@@ -312,6 +333,11 @@ export async function refreshOAuthToken(
clientId = process.env.X_CLIENT_ID
clientSecret = process.env.X_CLIENT_SECRET
break
case 'confluence':
tokenEndpoint = 'https://auth.atlassian.com/oauth/token'
clientId = process.env.CONFLUENCE_CLIENT_ID
clientSecret = process.env.CONFLUENCE_CLIENT_SECRET
break
default:
throw new Error(`Unsupported provider: ${provider}`)
}
+20 -24
View File
@@ -1,10 +1,9 @@
import { ToolConfig, ToolResponse } from '../types'
export interface ConfluenceRetrieveParams {
apiKey: string
accessToken: string
pageId: string
domain: string
email: string
domain: string
}
export interface ConfluenceRetrieveResponse extends ToolResponse {
@@ -16,18 +15,21 @@ export interface ConfluenceRetrieveResponse extends ToolResponse {
}
}
export const confluenceRetrieveTool: ToolConfig<ConfluenceRetrieveParams, ConfluenceRetrieveResponse> = {
export const confluenceRetrieveTool: ToolConfig<
ConfluenceRetrieveParams,
ConfluenceRetrieveResponse
> = {
id: 'confluence_retrieve',
name: 'Confluence Retrieve',
description: 'Retrieve content from Confluence pages using the Confluence API.',
version: '1.0.0',
params: {
apiKey: {
accessToken: {
type: 'string',
required: true,
requiredForToolCall: true,
description: 'Your Confluence API token',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
@@ -35,30 +37,24 @@ export const confluenceRetrieveTool: ToolConfig<ConfluenceRetrieveParams, Conflu
requiredForToolCall: true,
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
email: {
type: 'string',
required: true,
requiredForToolCall: true,
description: 'Your Atlassian email address',
},
pageId: {
type: 'string',
required: true,
description: 'Confluence page ID to retrieve',
}
},
},
request: {
url: (params: ConfluenceRetrieveParams) => {
return `https://${params.domain}/wiki/rest/api/content/${params.pageId}?expand=body.view`;
return `https://${params.domain}/wiki/rest/api/content/${params.pageId}?expand=body.view`
},
method: 'GET',
headers: (params: ConfluenceRetrieveParams) => {
return {
'Content-Type': 'application/json',
Authorization: `Basic ${Buffer.from(`${params.email}:${params.apiKey}`).toString('base64')}`,
};
}
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response) => {
@@ -66,15 +62,15 @@ export const confluenceRetrieveTool: ToolConfig<ConfluenceRetrieveParams, Conflu
if (!response.ok) {
throw new Error(data.message || 'Confluence API error')
}
const cleanContent = data.body.view.value
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/\s+/g, ' ')
.trim();
.replace(/\s+/g, ' ')
.trim()
return {
success: true,
@@ -83,12 +79,12 @@ export const confluenceRetrieveTool: ToolConfig<ConfluenceRetrieveParams, Conflu
pageId: data.id,
content: cleanContent,
title: data.title,
}
},
}
},
transformError: (error: any) => {
const message = error.message || 'Confluence retrieve failed'
return message
}
},
}