Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions apps/sim/app/api/tools/pinterest/boards/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('PinterestBoardsAPI')

interface PinterestBoard {
id: string
name: string
description?: string
privacy?: string
owner?: {
username: string
}
}

export async function POST(request: Request) {
try {
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId } = body

if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}

const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})

if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)

if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}

logger.info('Fetching Pinterest boards', { requestId })

const response = await fetch('https://api.pinterest.com/v5/boards', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})

if (!response.ok) {
const errorText = await response.text()
logger.error('Pinterest API error', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{ error: `Pinterest API error: ${response.status} - ${response.statusText}` },
{ status: response.status }
)
}

const data = await response.json()
const boards = (data.items || []).map((board: PinterestBoard) => ({
id: board.id,
name: board.name,
description: board.description,
privacy: board.privacy,
}))

logger.info(`Successfully fetched ${boards.length} Pinterest boards`, { requestId })
return NextResponse.json({ items: boards })
} catch (error) {
logger.error('Error processing Pinterest boards request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Pinterest boards', details: (error as Error).message },
{ status: 500 }
)
}
}
107 changes: 107 additions & 0 deletions apps/sim/blocks/blocks/pinterest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { PinterestIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { PinterestResponse } from '@/tools/pinterest/types'

export const PinterestBlock: BlockConfig<PinterestResponse> = {
type: 'pinterest',
name: 'Pinterest',
description: 'Create pins on your Pinterest boards',
authMode: AuthMode.OAuth,
longDescription: 'Create and share pins on Pinterest. Post images with titles, descriptions, and links to your boards.',
docsLink: 'https://docs.sim.ai/tools/pinterest',
category: 'tools',
bgColor: '#E60023',
icon: PinterestIcon,
subBlocks: [
{
id: 'credential',
title: 'Pinterest Account',
type: 'oauth-input',
serviceId: 'pinterest',
requiredScopes: ['boards:read', 'boards:write', 'pins:read', 'pins:write'],
placeholder: 'Select Pinterest account',
required: true,
},
{
id: 'board_id',
title: 'Board',
type: 'file-selector',
canonicalParamId: 'board_id',
serviceId: 'pinterest',
placeholder: 'Select a Pinterest board',
dependsOn: ['credential'],
required: true,
},
{
id: 'title',
title: 'Pin Title',
type: 'short-input',
placeholder: 'Enter pin title',
required: true,
},
{
id: 'description',
title: 'Pin Description',
type: 'long-input',
placeholder: 'Enter pin description',
required: true,
},
{
id: 'media_url',
title: 'Image URL',
type: 'short-input',
placeholder: 'Enter image URL',
required: true,
},
{
id: 'link',
title: 'Destination Link',
type: 'short-input',
placeholder: 'Enter destination URL (optional)',
required: false,
},
{
id: 'alt_text',
title: 'Alt Text',
type: 'short-input',
placeholder: 'Enter alt text for accessibility (optional)',
required: false,
},
],
tools: {
access: ['pinterest_create_pin'],
config: {
tool: () => 'pinterest_create_pin',
params: (inputs) => {
const { credential, ...rest } = inputs

return {
accessToken: credential,
board_id: rest.board_id,
title: rest.title,
description: rest.description,
media_url: rest.media_url,
link: rest.link,
alt_text: rest.alt_text,
}
},
},
},
inputs: {
credential: { type: 'string', description: 'Pinterest access token' },
board_id: { type: 'string', description: 'Board ID where the pin will be created' },
title: { type: 'string', description: 'Pin title' },
description: { type: 'string', description: 'Pin description' },
media_url: { type: 'string', description: 'Image URL for the pin' },
link: { type: 'string', description: 'Destination link when pin is clicked' },
alt_text: { type: 'string', description: 'Alt text for accessibility' },
},
outputs: {
success: { type: 'boolean', description: 'Whether the pin was created successfully' },
pin: { type: 'json', description: 'Full pin object' },
pin_id: { type: 'string', description: 'ID of the created pin' },
pin_url: { type: 'string', description: 'URL of the created pin' },
error: { type: 'string', description: 'Error message if operation failed' },
},
}
2 changes: 2 additions & 0 deletions apps/sim/blocks/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
import { LinearBlock } from '@/blocks/blocks/linear'
import { LinkedInBlock } from '@/blocks/blocks/linkedin'
import { LinkupBlock } from '@/blocks/blocks/linkup'
import { PinterestBlock } from '@/blocks/blocks/pinterest'
import { MailchimpBlock } from '@/blocks/blocks/mailchimp'
import { MailgunBlock } from '@/blocks/blocks/mailgun'
import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger'
Expand Down Expand Up @@ -205,6 +206,7 @@ export const registry: Record<string, BlockConfig> = {
linear: LinearBlock,
linkedin: LinkedInBlock,
linkup: LinkupBlock,
pinterest: PinterestBlock,
mailchimp: MailchimpBlock,
mailgun: MailgunBlock,
manual_trigger: ManualTriggerBlock,
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4302,6 +4302,12 @@ export function SpotifyIcon(props: SVGProps<SVGSVGElement>) {
)
}

export const PinterestIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} viewBox='0 0 24 24' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
<path d='M12 0C5.373 0 0 5.372 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738.098.119.112.224.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.631-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12 0-6.628-5.373-12-12-12z' />
</svg>
)

export function GrainIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 34' fill='none'>
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export default {
dbCredentials: {
url: env.DATABASE_URL,
},
} satisfies Config
} satisfies Config
31 changes: 31 additions & 0 deletions apps/sim/hooks/selectors/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,37 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
}))
},
},
'pinterest.boards': {
key: 'pinterest.boards',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'pinterest.boards',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const body = JSON.stringify({
credential: context.credentialId,
workflowId: context.workflowId,
})
const data = await fetchJson<{ items: { id: string; name: string; description?: string; privacy?: string }[] }>(
'/api/tools/pinterest/boards',
{
method: 'POST',
body,
}
)
return (data.items || []).map((board) => ({
id: board.id,
label: board.name,
meta: {
description: board.description,
privacy: board.privacy,
},
}))
},
},
}

export function getSelectorDefinition(key: SelectorKey): SelectorDefinition {
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/hooks/selectors/resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ function resolveFileSelector(
return { key: 'webflow.items', context, allowSearch: true }
}
return { key: null, context, allowSearch: true }
case 'pinterest':
return { key: 'pinterest.boards', context, allowSearch: true }
default:
return { key: null, context, allowSearch: true }
}
Expand Down
1 change: 1 addition & 0 deletions apps/sim/hooks/selectors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type SelectorKey =
| 'webflow.sites'
| 'webflow.collections'
| 'webflow.items'
| 'pinterest.boards'

export interface SelectorOption {
id: string
Expand Down
72 changes: 72 additions & 0 deletions apps/sim/lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export const auth = betterAuth({
'hubspot',
'linkedin',
'spotify',
'pinterest',

// Common SSO provider patterns
...SSO_TRUSTED_PROVIDERS,
Expand Down Expand Up @@ -1752,6 +1753,77 @@ export const auth = betterAuth({
},
},

// Pinterest provider
{
providerId: 'pinterest',
clientId: env.PINTEREST_CLIENT_ID as string,
clientSecret: env.PINTEREST_CLIENT_SECRET as string,
authorizationUrl: 'https://www.pinterest.com/oauth/',
tokenUrl: 'https://api.pinterest.com/v5/oauth/token',
userInfoUrl: 'https://api.pinterest.com/v5/user_account',
scopes: ['boards:read', 'boards:write', 'pins:read', 'pins:write'],
responseType: 'code',
authentication: 'basic',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/pinterest`,
getUserInfo: async (tokens) => {
try {
logger.info('Fetching Pinterest user profile', {
hasAccessToken: !!tokens.accessToken,
})

const response = await fetch('https://api.pinterest.com/v5/user_account', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
'Content-Type': 'application/json',
},
})

if (!response.ok) {
const errorBody = await response.text()
logger.error('Failed to fetch Pinterest user info', {
status: response.status,
statusText: response.statusText,
body: errorBody,
})

// Pinterest might not require user info - return minimal data
return {
id: `pinterest_${Date.now()}`,
name: 'Pinterest User',
email: `pinterest_${Date.now()}@pinterest.user`,
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
}
}

const profile = await response.json()
logger.info('Pinterest profile fetched successfully', { profile })

return {
id: profile.username || profile.id || `pinterest_${Date.now()}`,
name: profile.username || profile.business_name || 'Pinterest User',
email: `${profile.username || profile.id}@pinterest.user`,
emailVerified: true,
image: profile.profile_image || undefined,
createdAt: new Date(),
updatedAt: new Date(),
}
} catch (error) {
logger.error('Error in Pinterest getUserInfo:', { error })
// Return fallback user info instead of null
return {
id: `pinterest_${Date.now()}`,
name: 'Pinterest User',
email: `pinterest_${Date.now()}@pinterest.user`,
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
}
}
},
},

// Zoom provider
{
providerId: 'zoom',
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ export const env = createEnv({
WORDPRESS_CLIENT_SECRET: z.string().optional(), // WordPress.com OAuth client secret
SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID
SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret
PINTEREST_CLIENT_ID: z.string().optional(), // Pinterest OAuth client ID
PINTEREST_CLIENT_SECRET: z.string().optional(), // Pinterest OAuth client secret

// E2B Remote Code Execution
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution
Expand Down
Loading
Loading