mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(oauth): added atlassian OAuth2 credentials
This commit is contained in:
@@ -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 = ''
|
||||
|
||||
|
||||
+1
@@ -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,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
@@ -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
@@ -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
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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(/ /g, ' ')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/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
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user