Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7ef1150
fix(workflow-state, copilot): prevent copilot from setting undefined …
Sg312 Dec 18, 2025
78b7643
fix(condition): async execution isolated vm error (#2446)
icecrasher321 Dec 18, 2025
f3ad775
fix(auth): added same-origin validation to forget password route, add…
waleedlatif1 Dec 18, 2025
1720fa8
feat(compare-schema): ci check to make sure schema.ts never goes out …
icecrasher321 Dec 18, 2025
e83afc0
fixing the useWbehookManangement call to only call the loadwebhookorg…
Dec 18, 2025
9da19e8
fix(salesforce): updated to more flexible oauth that allows productio…
waleedlatif1 Dec 18, 2025
261aa3d
fixing a react component:
Dec 18, 2025
25f7ed2
feat(docs): added 404 page for the docs (#2450)
waleedlatif1 Dec 18, 2025
fbde64f
fixing lint errors
Dec 18, 2025
7575cd6
Merge pull request #2451 from simstudioai/improvement/SIM-514-useWebh…
Pbonmars-20031006 Dec 18, 2025
c23130a
Revert "fix(salesforce): updated to more flexible oauth that allows p…
waleedlatif1 Dec 18, 2025
04cd837
fix(notifs): inactivity polling filters, consolidate trigger types, m…
icecrasher321 Dec 18, 2025
f45730a
improvement(helm): added SSO and cloud storage variables to helm char…
waleedlatif1 Dec 18, 2025
36bdccb
fix(ui): fixed visibility issue on reset passowrd page (#2456)
waleedlatif1 Dec 18, 2025
0d30676
fix(blog): revert back to using next image tags in blog (#2458)
waleedlatif1 Dec 18, 2025
b5b12ba
fix(teams): webhook notifications crash (#2426)
CodeLoopdroid Dec 18, 2025
c675731
Merge branch 'main' into staging
icecrasher321 Dec 18, 2025
811c736
fix failing lint from os contributor (#2459)
icecrasher321 Dec 18, 2025
83d813a
improvement(copilot): add edge handle validation to copilot edit work…
Sg312 Dec 18, 2025
90c3c43
fix(blog): add back unoptimized tag, fix styling (#2461)
waleedlatif1 Dec 18, 2025
2a7f51a
adding clamps for subflow drag and drops of blocks (#2460)
Pbonmars-20031006 Dec 18, 2025
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
13 changes: 13 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ jobs:
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
run: bun run test

- name: Check schema and migrations are in sync
working-directory: packages/db
run: |
bunx drizzle-kit generate --config=./drizzle.config.ts
if [ -n "$(git status --porcelain ./migrations)" ]; then
echo "❌ Schema and migrations are out of sync!"
echo "Run 'cd packages/db && bunx drizzle-kit generate' and commit the new migrations."
git status --porcelain ./migrations
git diff ./migrations
exit 1
fi
echo "✅ Schema and migrations are in sync"

- name: Build application
env:
NODE_OPTIONS: '--no-warnings'
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"

Then run the migrations:
```bash
cd apps/sim # Required so drizzle picks correct .env file
cd packages/db # Required so drizzle picks correct .env file
bunx drizzle-kit migrate --config=./drizzle.config.ts
```

Expand Down
23 changes: 23 additions & 0 deletions apps/docs/app/[lang]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DocsBody, DocsPage } from 'fumadocs-ui/page'

export const metadata = {
title: 'Page Not Found',
}

export default function NotFound() {
return (
<DocsPage>
<DocsBody>
<div className='flex min-h-[60vh] flex-col items-center justify-center text-center'>
<h1 className='mb-4 bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] bg-clip-text font-bold text-8xl text-transparent'>
404
</h1>
<h2 className='mb-2 font-semibold text-2xl text-foreground'>Page Not Found</h2>
<p className='text-muted-foreground'>
The page you're looking for doesn't exist or has been moved.
</p>
</div>
</DocsBody>
</DocsPage>
)
}
4 changes: 2 additions & 2 deletions apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -573,10 +573,10 @@ export default function LoginPage({
<Dialog open={forgotPasswordOpen} onOpenChange={setForgotPasswordOpen}>
<DialogContent className='auth-card auth-card-shadow max-w-[540px] rounded-[10px] border backdrop-blur-sm'>
<DialogHeader>
<DialogTitle className='auth-text-primary font-semibold text-xl tracking-tight'>
<DialogTitle className='font-semibold text-black text-xl tracking-tight'>
Reset Password
</DialogTitle>
<DialogDescription className='auth-text-secondary text-sm'>
<DialogDescription className='text-muted-foreground text-sm'>
Enter your email address and we'll send you a link to reset your password if your
account exists.
</DialogDescription>
Expand Down
68 changes: 2 additions & 66 deletions apps/sim/app/(landing)/studio/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Image from 'next/image'
import Link from 'next/link'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { getAllPostMeta } from '@/lib/blog/registry'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { PostGrid } from '@/app/(landing)/studio/post-grid'

export const revalidate = 3600

Expand All @@ -18,7 +17,6 @@ export default async function StudioIndex({
const all = await getAllPostMeta()
const filtered = tag ? all.filter((p) => p.tags.includes(tag)) : all

// Sort to ensure featured post is first on page 1
const sorted =
pageNum === 1
? filtered.sort((a, b) => {
Expand Down Expand Up @@ -63,69 +61,7 @@ export default async function StudioIndex({
</div> */}

{/* Grid layout for consistent rows */}
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
{posts.map((p, i) => {
return (
<Link key={p.slug} href={`/studio/${p.slug}`} className='group flex flex-col'>
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 transition-colors duration-300 hover:border-gray-300'>
<Image
src={p.ogImage}
alt={p.title}
width={800}
height={450}
className='h-48 w-full object-cover'
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
loading='lazy'
unoptimized
/>
<div className='flex flex-1 flex-col p-4'>
<div className='mb-2 text-gray-600 text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<h3 className='shine-text mb-1 font-medium text-lg leading-tight'>{p.title}</h3>
<p className='mb-3 line-clamp-3 flex-1 text-gray-700 text-sm'>{p.description}</p>
<div className='flex items-center gap-2'>
<div className='-space-x-1.5 flex'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 3)
.map((author, idx) => (
<Avatar key={idx} className='size-4 border border-white'>
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
<AvatarFallback className='border border-white bg-gray-100 text-[10px] text-gray-600'>
{author?.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
))}
</div>
<span className='text-gray-600 text-xs'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 2)
.map((a) => a?.name)
.join(', ')}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length > 2 && (
<>
{' '}
and{' '}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2}{' '}
other
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2 >
1
? 's'
: ''}
</>
)}
</span>
</div>
</div>
</div>
</Link>
)
})}
</div>
<PostGrid posts={posts} />

{totalPages > 1 && (
<div className='mt-10 flex items-center justify-center gap-3'>
Expand Down
90 changes: 90 additions & 0 deletions apps/sim/app/(landing)/studio/post-grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client'

import Image from 'next/image'
import Link from 'next/link'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'

interface Author {
id: string
name: string
avatarUrl?: string
url?: string
}

interface Post {
slug: string
title: string
description: string
date: string
ogImage: string
author: Author
authors?: Author[]
featured?: boolean
}

export function PostGrid({ posts }: { posts: Post[] }) {
return (
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
{posts.map((p, index) => (
<Link key={p.slug} href={`/studio/${p.slug}`} className='group flex flex-col'>
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 transition-colors duration-300 hover:border-gray-300'>
{/* Image container with fixed aspect ratio to prevent layout shift */}
<div className='relative aspect-video w-full overflow-hidden'>
<Image
src={p.ogImage}
alt={p.title}
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
unoptimized
priority={index < 6}
loading={index < 6 ? undefined : 'lazy'}
fill
style={{ objectFit: 'cover' }}
/>
</div>
<div className='flex flex-1 flex-col p-4'>
<div className='mb-2 text-gray-600 text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<h3 className='shine-text mb-1 font-medium text-lg leading-tight'>{p.title}</h3>
<p className='mb-3 line-clamp-3 flex-1 text-gray-700 text-sm'>{p.description}</p>
<div className='flex items-center gap-2'>
<div className='-space-x-1.5 flex'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 3)
.map((author, idx) => (
<Avatar key={idx} className='size-4 border border-white'>
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
<AvatarFallback className='border border-white bg-gray-100 text-[10px] text-gray-600'>
{author?.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
))}
</div>
<span className='text-gray-600 text-xs'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 2)
.map((a) => a?.name)
.join(', ')}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length > 2 && (
<>
{' '}
and {(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2}{' '}
other
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2 > 1
? 's'
: ''}
</>
)}
</span>
</div>
</div>
</div>
</Link>
))}
</div>
)
}
1 change: 1 addition & 0 deletions apps/sim/app/_shell/providers/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
pathname === '/' ||
pathname.startsWith('/login') ||
pathname.startsWith('/signup') ||
pathname.startsWith('/reset-password') ||
pathname.startsWith('/sso') ||
pathname.startsWith('/terms') ||
pathname.startsWith('/privacy') ||
Expand Down
21 changes: 21 additions & 0 deletions apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -759,3 +759,24 @@ input[type="search"]::-ms-clear {
--surface-elevated: #202020;
}
}

/**
* Remove backticks from inline code in prose (Tailwind Typography default)
*/
.prose code::before,
.prose code::after {
content: none !important;
}

/**
* Remove underlines from heading anchor links in prose
*/
.prose h1 a,
.prose h2 a,
.prose h3 a,
.prose h4 a,
.prose h5 a,
.prose h6 a {
text-decoration: none !important;
color: inherit !important;
}
12 changes: 11 additions & 1 deletion apps/sim/app/api/auth/accounts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,17 @@ export async function GET(request: NextRequest) {
.from(account)
.where(and(...whereConditions))

return NextResponse.json({ accounts })
// Use the user's email as the display name (consistent with credential selector)
const userEmail = session.user.email

const accountsWithDisplayName = accounts.map((acc) => ({
id: acc.id,
accountId: acc.accountId,
providerId: acc.providerId,
displayName: userEmail || acc.providerId,
}))

return NextResponse.json({ accounts: accountsWithDisplayName })
} catch (error) {
logger.error('Failed to fetch accounts', { error })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
Expand Down
34 changes: 31 additions & 3 deletions apps/sim/app/api/auth/forget-password/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest, setupAuthApiMocks } from '@/app/api/__test-utils__/utils'

vi.mock('@/lib/core/utils/urls', () => ({
getBaseUrl: vi.fn(() => 'https://app.example.com'),
}))

describe('Forget Password API Route', () => {
beforeEach(() => {
vi.resetModules()
Expand All @@ -15,7 +19,7 @@ describe('Forget Password API Route', () => {
vi.clearAllMocks()
})

it('should send password reset email successfully', async () => {
it('should send password reset email successfully with same-origin redirectTo', async () => {
setupAuthApiMocks({
operations: {
forgetPassword: { success: true },
Expand All @@ -24,7 +28,7 @@ describe('Forget Password API Route', () => {

const req = createMockRequest('POST', {
email: 'test@example.com',
redirectTo: 'https://example.com/reset',
redirectTo: 'https://app.example.com/reset',
})

const { POST } = await import('@/app/api/auth/forget-password/route')
Expand All @@ -39,12 +43,36 @@ describe('Forget Password API Route', () => {
expect(auth.auth.api.forgetPassword).toHaveBeenCalledWith({
body: {
email: 'test@example.com',
redirectTo: 'https://example.com/reset',
redirectTo: 'https://app.example.com/reset',
},
method: 'POST',
})
})

it('should reject external redirectTo URL', async () => {
setupAuthApiMocks({
operations: {
forgetPassword: { success: true },
},
})

const req = createMockRequest('POST', {
email: 'test@example.com',
redirectTo: 'https://evil.com/phishing',
})

const { POST } = await import('@/app/api/auth/forget-password/route')

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(400)
expect(data.message).toBe('Redirect URL must be a valid same-origin URL')

const auth = await import('@/lib/auth')
expect(auth.auth.api.forgetPassword).not.toHaveBeenCalled()
})

it('should send password reset email without redirectTo', async () => {
setupAuthApiMocks({
operations: {
Expand Down
10 changes: 8 additions & 2 deletions apps/sim/app/api/auth/forget-password/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { auth } from '@/lib/auth'
import { isSameOrigin } from '@/lib/core/utils/validation'
import { createLogger } from '@/lib/logs/console/logger'

export const dynamic = 'force-dynamic'
Expand All @@ -13,10 +14,15 @@ const forgetPasswordSchema = z.object({
.email('Please provide a valid email address'),
redirectTo: z
.string()
.url('Redirect URL must be a valid URL')
.optional()
.or(z.literal(''))
.transform((val) => (val === '' ? undefined : val)),
.transform((val) => (val === '' || val === undefined ? undefined : val))
.refine(
(val) => val === undefined || (z.string().url().safeParse(val).success && isSameOrigin(val)),
{
message: 'Redirect URL must be a valid same-origin URL',
}
),
})

export async function POST(request: NextRequest) {
Expand Down
Loading
Loading