chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,65 @@
import { createLogger } from '@sim/logger'
import { generateShortId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { authorizeTrelloContract } from '@/lib/api/contracts/oauth-connections'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getCanonicalScopesForProvider } from '@/lib/oauth/utils'
const logger = createLogger('TrelloAuthorize')
export const dynamic = 'force-dynamic'
const TRELLO_STATE_COOKIE = 'trello_oauth_state'
const TRELLO_STATE_COOKIE_PATH = '/api/auth/trello'
const TRELLO_STATE_COOKIE_MAX_AGE_SECONDS = 60 * 10
export const GET = withRouteHandler(async (request: NextRequest) => {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(authorizeTrelloContract, request, {})
if (!parsed.success) return parsed.response
const apiKey = env.TRELLO_API_KEY
if (!apiKey) {
logger.error('TRELLO_API_KEY not configured')
return NextResponse.json({ error: 'Trello API key not configured' }, { status: 500 })
}
const baseUrl = getBaseUrl()
const state = generateShortId(32)
const returnUrl = new URL('/api/auth/trello/callback', baseUrl)
returnUrl.searchParams.set('state', state)
const scope = getCanonicalScopesForProvider('trello').join(',')
const authUrl = new URL('https://trello.com/1/authorize')
authUrl.searchParams.set('key', apiKey)
authUrl.searchParams.set('name', 'Sim Studio')
authUrl.searchParams.set('expiration', 'never')
authUrl.searchParams.set('callback_method', 'fragment')
authUrl.searchParams.set('response_type', 'token')
authUrl.searchParams.set('scope', scope)
authUrl.searchParams.set('return_url', returnUrl.toString())
const response = NextResponse.redirect(authUrl.toString())
response.cookies.set(TRELLO_STATE_COOKIE, state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: TRELLO_STATE_COOKIE_MAX_AGE_SECONDS,
path: TRELLO_STATE_COOKIE_PATH,
})
return response
} catch (error) {
logger.error('Error initiating Trello authorization:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
})
@@ -0,0 +1,179 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { trelloCallbackContract } from '@/lib/api/contracts/oauth-connections'
import { parseRequest } from '@/lib/api/server'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('TrelloCallback')
export const dynamic = 'force-dynamic'
const TRELLO_STATE_COOKIE = 'trello_oauth_state'
function escapeForJsString(value: string): string {
return value.replace(/[\\'"<>&\r\n\u2028\u2029]/g, (ch) => {
return `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`
})
}
function renderErrorPage(baseUrl: string, redirectQuery: string) {
return new NextResponse(
`<!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>`,
{
status: 400,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store, no-cache, must-revalidate',
},
}
)
}
export const GET = withRouteHandler(async (request: NextRequest) => {
const parsed = await parseRequest(trelloCallbackContract, request, {})
if (!parsed.success) return parsed.response
const baseUrl = getBaseUrl()
const queryState = parsed.data.query.state
const cookieState = request.cookies.get(TRELLO_STATE_COOKIE)?.value
if (!queryState || !cookieState || queryState !== cookieState) {
logger.warn('Trello callback rejected: state mismatch or missing state', {
hasQueryState: Boolean(queryState),
hasCookieState: Boolean(cookieState),
})
const response = renderErrorPage(baseUrl, 'error=trello_state_mismatch')
response.cookies.delete({ name: TRELLO_STATE_COOKIE, path: '/api/auth/trello' })
return response
}
const safeState = escapeForJsString(queryState)
return new NextResponse(
`<!DOCTYPE html>
<html>
<head>
<title>Connecting to Trello...</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #0052CC 0%, #0079BF 100%);
}
.container {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
text-align: center;
max-width: 400px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #0052CC;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
color: #ef4444;
margin-top: 1rem;
}
h2 {
color: #111827;
margin: 0 0 0.5rem 0;
}
p {
color: #6b7280;
margin: 0;
}
</style>
</head>
<body>
<div class="container">
<div class="spinner"></div>
<h2>Connecting to Trello</h2>
<p id="status">Processing authorization...</p>
<p id="error" class="error" style="display:none;"></p>
</div>
<script>
(function() {
const statusEl = document.getElementById('status');
const errorEl = document.getElementById('error');
try {
const fragment = window.location.hash.substring(1);
const params = new URLSearchParams(fragment);
const token = params.get('token');
const authError = params.get('error');
if (authError) {
throw new Error(authError);
}
if (!token) {
throw new Error('No token received from Trello');
}
statusEl.textContent = 'Saving your connection...';
fetch('${baseUrl}/api/auth/trello/store', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ token: token, state: '${safeState}' })
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusEl.textContent = 'Success! Redirecting...';
setTimeout(function() {
window.location.href = '${baseUrl}/workspace?trello_connected=true';
}, 500);
} else {
throw new Error(data.error || 'Failed to save connection');
}
})
.catch(error => {
errorEl.textContent = error.message || 'Failed to save connection';
errorEl.style.display = 'block';
statusEl.textContent = 'Connection failed';
setTimeout(function() {
window.location.href = '${baseUrl}/workspace?error=trello_failed';
}, 3000);
});
} catch (error) {
errorEl.textContent = error.message || 'Authorization failed';
errorEl.style.display = 'block';
statusEl.textContent = 'Connection failed';
setTimeout(function() {
window.location.href = '${baseUrl}/workspace?error=trello_auth_failed';
}, 3000);
}
})();
</script>
</body>
</html>`,
{
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store, no-cache, must-revalidate',
},
}
)
})
+153
View File
@@ -0,0 +1,153 @@
import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { storeTrelloTokenContract } from '@/lib/api/contracts/oauth-connections'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processCredentialDraft } from '@/lib/credentials/draft-processor'
import { getCanonicalScopesForProvider } from '@/lib/oauth/utils'
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
const logger = createLogger('TrelloStore')
export const dynamic = 'force-dynamic'
const TRELLO_STATE_COOKIE = 'trello_oauth_state'
const TRELLO_STATE_COOKIE_PATH = '/api/auth/trello'
function clearStateCookie(response: NextResponse) {
response.cookies.delete({ name: TRELLO_STATE_COOKIE, path: TRELLO_STATE_COOKIE_PATH })
return response
}
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn('Unauthorized attempt to store Trello token')
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(storeTrelloTokenContract, request, {})
if (!parsed.success) return parsed.response
const { token, state } = parsed.data.body
const cookieState = request.cookies.get(TRELLO_STATE_COOKIE)?.value
if (!cookieState || cookieState !== state) {
logger.warn('Trello store rejected: state mismatch', {
hasCookieState: Boolean(cookieState),
userId: session.user.id,
})
return clearStateCookie(
NextResponse.json(
{ success: false, error: 'Invalid or expired authorization state' },
{ status: 400 }
)
)
}
const apiKey = env.TRELLO_API_KEY
if (!apiKey) {
logger.error('TRELLO_API_KEY not configured')
return NextResponse.json({ success: false, error: 'Trello not configured' }, { status: 500 })
}
const scope = getCanonicalScopesForProvider('trello').join(',')
const validationUrl = `https://api.trello.com/1/members/me?key=${apiKey}&token=${token}&fields=id,username,fullName`
const userResponse = await fetch(validationUrl, {
headers: { Accept: 'application/json' },
})
if (!userResponse.ok) {
const errorText = await userResponse.text()
logger.error('Invalid Trello token', {
status: userResponse.status,
error: errorText,
})
return NextResponse.json(
{ success: false, error: `Invalid Trello token: ${errorText}` },
{ status: 400 }
)
}
const trelloUser = (await userResponse.json().catch(() => null)) as { id?: string } | null
if (typeof trelloUser?.id !== 'string' || trelloUser.id.trim().length === 0) {
logger.error('Trello validation response did not include a valid member id', {
response: trelloUser,
})
return NextResponse.json(
{ success: false, error: 'Invalid Trello member response' },
{ status: 502 }
)
}
const existing = await db.query.account.findFirst({
where: and(
eq(account.userId, session.user.id),
eq(account.providerId, 'trello'),
eq(account.accountId, trelloUser.id)
),
})
const now = new Date()
if (existing) {
await db
.update(account)
.set({
accessToken: token,
accountId: trelloUser.id,
scope,
updatedAt: now,
})
.where(eq(account.id, existing.id))
} else {
await safeAccountInsert(
{
id: `trello_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'trello',
accountId: trelloUser.id,
accessToken: token,
scope,
createdAt: now,
updatedAt: now,
},
{ provider: 'Trello', identifier: trelloUser.id }
)
}
const persisted =
existing ??
(await db.query.account.findFirst({
where: and(
eq(account.userId, session.user.id),
eq(account.providerId, 'trello'),
eq(account.accountId, trelloUser.id)
),
}))
if (persisted) {
try {
await processCredentialDraft({
userId: session.user.id,
providerId: 'trello',
accountId: persisted.id,
})
} catch (error) {
logger.error('Failed to process credential draft for Trello', { error })
}
}
return clearStateCookie(NextResponse.json({ success: true }))
} catch (error) {
logger.error('Error storing Trello token:', error)
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 })
}
})