Skip to content

Commit 817ca0d

Browse files
committed
credentials continue
1 parent e1f2bf8 commit 817ca0d

33 files changed

Lines changed: 2688 additions & 308 deletions

apps/sim/app/api/auth/trello/authorize/route.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { parseRequest } from '@/lib/api/server'
66
import { getSession } from '@/lib/auth'
77
import { env } from '@/lib/core/config/env'
88
import { getBaseUrl } from '@/lib/core/utils/urls'
9+
import { isSameOrigin } from '@/lib/core/utils/validation'
910
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1011
import { getCanonicalScopesForProvider } from '@/lib/oauth/utils'
1112

@@ -14,6 +15,7 @@ const logger = createLogger('TrelloAuthorize')
1415
export const dynamic = 'force-dynamic'
1516

1617
const TRELLO_STATE_COOKIE = 'trello_oauth_state'
18+
const TRELLO_RETURN_URL_COOKIE = 'trello_return_url'
1719
const TRELLO_STATE_COOKIE_PATH = '/api/auth/trello'
1820
const TRELLO_STATE_COOKIE_MAX_AGE_SECONDS = 60 * 10
1921

@@ -26,6 +28,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
2628

2729
const parsed = await parseRequest(authorizeTrelloContract, request, {})
2830
if (!parsed.success) return parsed.response
31+
const { returnUrl: requestedReturnUrl } = parsed.data.query
2932

3033
const apiKey = env.TRELLO_API_KEY
3134

@@ -57,6 +60,20 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
5760
maxAge: TRELLO_STATE_COOKIE_MAX_AGE_SECONDS,
5861
path: TRELLO_STATE_COOKIE_PATH,
5962
})
63+
if (requestedReturnUrl && isSameOrigin(requestedReturnUrl)) {
64+
response.cookies.set(TRELLO_RETURN_URL_COOKIE, requestedReturnUrl, {
65+
httpOnly: true,
66+
secure: process.env.NODE_ENV === 'production',
67+
sameSite: 'lax',
68+
maxAge: TRELLO_STATE_COOKIE_MAX_AGE_SECONDS,
69+
path: TRELLO_STATE_COOKIE_PATH,
70+
})
71+
} else {
72+
response.cookies.delete({
73+
name: TRELLO_RETURN_URL_COOKIE,
74+
path: TRELLO_STATE_COOKIE_PATH,
75+
})
76+
}
6077
return response
6178
} catch (error) {
6279
logger.error('Error initiating Trello authorization:', error)

apps/sim/app/api/auth/trello/callback/route.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,32 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { trelloCallbackContract } from '@/lib/api/contracts/oauth-connections'
44
import { parseRequest } from '@/lib/api/server'
55
import { getBaseUrl } from '@/lib/core/utils/urls'
6+
import { isSameOrigin } from '@/lib/core/utils/validation'
67
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
78

89
const logger = createLogger('TrelloCallback')
910

1011
export const dynamic = 'force-dynamic'
1112

1213
const TRELLO_STATE_COOKIE = 'trello_oauth_state'
14+
const TRELLO_RETURN_URL_COOKIE = 'trello_return_url'
15+
const TRELLO_COOKIE_PATH = '/api/auth/trello'
1316

1417
function escapeForJsString(value: string): string {
1518
return value.replace(/[\\'"<>&\r\n\u2028\u2029]/g, (ch) => {
1619
return `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`
1720
})
1821
}
1922

20-
function renderErrorPage(baseUrl: string, redirectQuery: string) {
23+
function withResultParam(returnUrl: string, key: string, value: string): string {
24+
const url = new URL(returnUrl)
25+
url.searchParams.set(key, value)
26+
return url.toString()
27+
}
28+
29+
function renderErrorPage(redirectUrl: string) {
2130
return new NextResponse(
22-
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Trello connection failed</title></head><body><script>window.location.href=${JSON.stringify(`${baseUrl}/workspace?${redirectQuery}`)};</script><p>Trello connection failed. Redirecting...</p></body></html>`,
31+
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Trello connection failed</title></head><body><script>window.location.href=${JSON.stringify(redirectUrl).replace(/</g, '\\u003c')};</script><p>Trello connection failed. Redirecting...</p></body></html>`,
2332
{
2433
status: 400,
2534
headers: {
@@ -35,6 +44,11 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
3544
if (!parsed.success) return parsed.response
3645

3746
const baseUrl = getBaseUrl()
47+
const requestedReturnUrl = request.cookies.get(TRELLO_RETURN_URL_COOKIE)?.value
48+
const returnUrl =
49+
requestedReturnUrl && isSameOrigin(requestedReturnUrl)
50+
? requestedReturnUrl
51+
: `${baseUrl}/workspace`
3852
const queryState = parsed.data.query.state
3953
const cookieState = request.cookies.get(TRELLO_STATE_COOKIE)?.value
4054

@@ -43,12 +57,20 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
4357
hasQueryState: Boolean(queryState),
4458
hasCookieState: Boolean(cookieState),
4559
})
46-
const response = renderErrorPage(baseUrl, 'error=trello_state_mismatch')
47-
response.cookies.delete({ name: TRELLO_STATE_COOKIE, path: '/api/auth/trello' })
60+
const response = renderErrorPage(withResultParam(returnUrl, 'error', 'trello_state_mismatch'))
61+
response.cookies.delete({ name: TRELLO_STATE_COOKIE, path: TRELLO_COOKIE_PATH })
62+
response.cookies.delete({ name: TRELLO_RETURN_URL_COOKIE, path: TRELLO_COOKIE_PATH })
4863
return response
4964
}
5065

5166
const safeState = escapeForJsString(queryState)
67+
const successReturnUrl = escapeForJsString(withResultParam(returnUrl, 'trello_connected', 'true'))
68+
const storeFailureReturnUrl = escapeForJsString(
69+
withResultParam(returnUrl, 'error', 'trello_failed')
70+
)
71+
const authFailureReturnUrl = escapeForJsString(
72+
withResultParam(returnUrl, 'error', 'trello_auth_failed')
73+
)
5274

5375
return new NextResponse(
5476
`<!DOCTYPE html>
@@ -142,7 +164,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
142164
if (data.success) {
143165
statusEl.textContent = 'Success! Redirecting...';
144166
setTimeout(function() {
145-
window.location.href = '${baseUrl}/workspace?trello_connected=true';
167+
window.location.href = '${successReturnUrl}';
146168
}, 500);
147169
} else {
148170
throw new Error(data.error || 'Failed to save connection');
@@ -153,7 +175,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
153175
errorEl.style.display = 'block';
154176
statusEl.textContent = 'Connection failed';
155177
setTimeout(function() {
156-
window.location.href = '${baseUrl}/workspace?error=trello_failed';
178+
window.location.href = '${storeFailureReturnUrl}';
157179
}, 3000);
158180
});
159181
@@ -162,7 +184,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
162184
errorEl.style.display = 'block';
163185
statusEl.textContent = 'Connection failed';
164186
setTimeout(function() {
165-
window.location.href = '${baseUrl}/workspace?error=trello_auth_failed';
187+
window.location.href = '${authFailureReturnUrl}';
166188
}, 3000);
167189
}
168190
})();

apps/sim/app/api/auth/trello/store/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ const logger = createLogger('TrelloStore')
1717
export const dynamic = 'force-dynamic'
1818

1919
const TRELLO_STATE_COOKIE = 'trello_oauth_state'
20+
const TRELLO_RETURN_URL_COOKIE = 'trello_return_url'
2021
const TRELLO_STATE_COOKIE_PATH = '/api/auth/trello'
2122

2223
function clearStateCookie(response: NextResponse) {
2324
response.cookies.delete({ name: TRELLO_STATE_COOKIE, path: TRELLO_STATE_COOKIE_PATH })
25+
response.cookies.delete({ name: TRELLO_RETURN_URL_COOKIE, path: TRELLO_STATE_COOKIE_PATH })
2426
return response
2527
}
2628

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,13 @@ export function ToolCallItem({
206206
return (
207207
<div className='pl-6'>
208208
<CredentialDisplay
209-
data={{
210-
type: 'terminal_handoff',
211-
value: terminalHandoff.terminalId,
212-
name: terminalHandoff.reason,
213-
}}
209+
data={[
210+
{
211+
type: 'terminal_handoff',
212+
value: terminalHandoff.terminalId,
213+
name: terminalHandoff.reason,
214+
},
215+
]}
214216
/>
215217
</div>
216218
)
@@ -220,7 +222,7 @@ export function ToolCallItem({
220222
const reason = typeof params?.reason === 'string' ? params.reason.trim() : ''
221223
return (
222224
<div className='pl-6'>
223-
<CredentialDisplay data={{ type: 'browser_takeover', name: reason }} />
225+
<CredentialDisplay data={[{ type: 'browser_takeover', name: reason }]} />
224226
</div>
225227
)
226228
}

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { extractTextContent } from '@/lib/core/utils/react-node-text'
1717
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
1818
import {
1919
type ContentSegment,
20+
type CredentialSubmissionPayload,
2021
parseSpecialTags,
2122
SpecialTags,
2223
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
@@ -394,9 +395,12 @@ const MARKDOWN_COMPONENTS = {
394395

395396
interface ChatContentProps {
396397
content: string
398+
messageId?: string
397399
isStreaming?: boolean
398400
/** Transcript-derived answers for this message's question card (renders the recap). */
399401
questionAnswers?: string[]
402+
/** Transcript-derived status payload for this message's credential card. */
403+
credentialSubmission?: CredentialSubmissionPayload
400404
onOptionSelect?: (id: string) => void
401405
onQuestionDismiss?: () => void
402406
onWorkspaceResourceSelect?: (resource: WorkspaceResourceRef) => void
@@ -412,8 +416,10 @@ interface ChatContentProps {
412416

413417
function ChatContentInner({
414418
content,
419+
messageId,
415420
isStreaming = false,
416421
questionAnswers,
422+
credentialSubmission,
417423
onOptionSelect,
418424
onQuestionDismiss,
419425
onWorkspaceResourceSelect,
@@ -636,7 +642,9 @@ function ChatContentInner({
636642
<SpecialTags
637643
key={`special-${group.index}`}
638644
segment={group.segment}
645+
interactionId={`${messageId ?? 'message'}:${group.index}`}
639646
questionAnswers={questionAnswers}
647+
credentialSubmission={credentialSubmission}
640648
onOptionSelect={onOptionSelect}
641649
onQuestionDismiss={onQuestionDismiss}
642650
/>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { forwardRef, type InputHTMLAttributes, type MouseEventHandler, type ReactNode } from 'react'
2+
import { ArrowRight, cn } from '@sim/emcn'
3+
4+
export const INTERACTION_CARD_ROW_CLASSES =
5+
'flex items-center gap-2 border-[var(--border)] px-2 py-2 text-left transition-colors'
6+
7+
export const INTERACTION_CARD_TEXT_INPUT_CLASSES =
8+
'min-w-0 flex-1 border-0 bg-transparent p-0 text-[var(--text-body)] text-sm outline-none placeholder:text-[var(--text-muted)] disabled:cursor-not-allowed'
9+
10+
export interface InteractionCardRecapItem {
11+
label: string
12+
values: readonly string[]
13+
}
14+
15+
interface InteractionCardProps {
16+
children: ReactNode
17+
title?: ReactNode
18+
actions?: ReactNode
19+
className?: string
20+
}
21+
22+
/**
23+
* Shared chat-inline card chrome for terminal UI tags that need user input.
24+
* Question choices and credential controls use this same shell so their
25+
* spacing, border, surface, and header remain visually identical.
26+
*/
27+
export function InteractionCard({ children, title, actions, className }: InteractionCardProps) {
28+
return (
29+
<div
30+
className={cn(
31+
'rounded-2xl border border-[var(--border-1)] bg-[var(--white)] px-2.5 py-2 dark:bg-[var(--surface-4)]',
32+
className
33+
)}
34+
>
35+
{title !== undefined && (
36+
<div className='flex items-center justify-between gap-2 px-2 py-2'>
37+
<p className='min-w-0 flex-1 break-words text-[var(--text-primary)] text-sm'>{title}</p>
38+
{actions}
39+
</div>
40+
)}
41+
{children}
42+
</div>
43+
)
44+
}
45+
46+
interface InteractionCardRecapProps {
47+
items: readonly InteractionCardRecapItem[]
48+
}
49+
50+
/** Shared answered-state layout used by questions and credential requests. */
51+
export function InteractionCardRecap({ items }: InteractionCardRecapProps) {
52+
return (
53+
<InteractionCard>
54+
{items.map((item, index) => (
55+
<div key={`${item.label}-${index}`} className='px-2 py-2'>
56+
<p className='text-[var(--text-primary)] text-sm'>{item.label}</p>
57+
<div className='mt-1.5 flex flex-col gap-1 text-[var(--text-muted)] text-sm'>
58+
{item.values.map((value, valueIndex) => (
59+
<p key={`${value}-${valueIndex}`}>{value}</p>
60+
))}
61+
</div>
62+
</div>
63+
))}
64+
</InteractionCard>
65+
)
66+
}
67+
68+
export interface InteractionCardInputRowProps
69+
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> {
70+
divided?: boolean
71+
leading?: ReactNode
72+
trailing?: ReactNode
73+
inputClassName?: string
74+
}
75+
76+
/** Shared inline-input row used by question free text and credential secrets. */
77+
export const InteractionCardInputRow = forwardRef<HTMLInputElement, InteractionCardInputRowProps>(
78+
({ divided = false, leading, trailing, inputClassName, ...inputProps }, ref) => (
79+
<div className={cn(INTERACTION_CARD_ROW_CLASSES, divided && 'border-t')}>
80+
{leading}
81+
<input
82+
ref={ref}
83+
className={cn(INTERACTION_CARD_TEXT_INPUT_CLASSES, inputClassName)}
84+
{...inputProps}
85+
/>
86+
{trailing}
87+
</div>
88+
)
89+
)
90+
InteractionCardInputRow.displayName = 'InteractionCardInputRow'
91+
92+
interface InteractionCardActionRowProps {
93+
label: string
94+
leading?: ReactNode
95+
disabled?: boolean
96+
onClick: MouseEventHandler<HTMLButtonElement>
97+
}
98+
99+
/** Shared terminal action row used for question and credential submission. */
100+
export function InteractionCardActionRow({
101+
label,
102+
leading,
103+
disabled = false,
104+
onClick,
105+
}: InteractionCardActionRowProps) {
106+
return (
107+
<button
108+
type='button'
109+
disabled={disabled}
110+
onClick={onClick}
111+
className={cn(
112+
INTERACTION_CARD_ROW_CLASSES,
113+
'border-t',
114+
disabled ? 'cursor-not-allowed' : 'hover-hover:bg-[var(--surface-5)]'
115+
)}
116+
>
117+
{leading}
118+
<span
119+
className={cn(
120+
'flex-1 truncate text-sm',
121+
disabled ? 'text-[var(--text-muted)]' : 'text-[var(--text-body)]'
122+
)}
123+
>
124+
{label}
125+
</span>
126+
<ArrowRight className='size-[16px] shrink-0 text-[var(--text-icon)]' />
127+
</button>
128+
)
129+
}

0 commit comments

Comments
 (0)