Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ BETTER_AUTH_URL=http://localhost:3000
# NextJS (Required)
NEXT_PUBLIC_APP_URL=http://localhost:3000
# INTERNAL_API_BASE_URL=http://sim-app.default.svc.cluster.local:3000 # Optional: internal URL for server-side /api self-calls; defaults to NEXT_PUBLIC_APP_URL
# NEXT_PUBLIC_ENABLE_LANDING_PAGE=true # Optional public-page flag. Unset = enabled; set false to disable /
# NEXT_PUBLIC_ENABLE_STUDIO_PAGES=true # Optional public-page flag. Unset = enabled; set false to disable /studio* and studio feeds
# NEXT_PUBLIC_ENABLE_CHANGELOG_PAGE=true # Optional public-page flag. Unset = enabled; set false to disable /changelog
# NEXT_PUBLIC_ENABLE_LEGAL_PAGES=true # Optional public-page flag. Unset = enabled; set false to disable /terms and /privacy
# NEXT_PUBLIC_ENABLE_TEMPLATES_PAGES=true # Optional public-page flag. Unset = enabled; set false to disable /templates and /templates/[id]
# NEXT_PUBLIC_ENABLE_CAREERS_LINK=true # Optional public-page flag. Unset = enabled; set false to disable careers links and /careers redirect

# Security (Required)
ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate, used to encrypt environment variables
Expand Down
178 changes: 178 additions & 0 deletions apps/sim/app/(auth)/auth-legal-links.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* @vitest-environment node
*/

import { renderToStaticMarkup } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockGetEnv, mockFeatureFlags } = vi.hoisted(() => ({
mockGetEnv: vi.fn((key: string) => {
if (key === 'NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED') return 'true'
if (key === 'NEXT_PUBLIC_SSO_ENABLED') return 'false'
return undefined
}),
mockFeatureFlags: {
getAuthTermsLinkConfig: (() => ({ href: '/terms', isExternal: false })) as () => {
href: string
isExternal: boolean
} | null,
getAuthPrivacyLinkConfig: (() => ({ href: '/privacy', isExternal: false })) as () => {
href: string
isExternal: boolean
} | null,
},
}))

vi.mock('next/link', () => ({
default: ({ href, children, ...props }: React.ComponentProps<'a'>) => (
<a href={typeof href === 'string' ? href : ''} {...props}>
{children}
</a>
),
}))

vi.mock('next/navigation', () => ({
useRouter: () => ({ push: vi.fn() }),
useSearchParams: () => new URLSearchParams(),
}))

vi.mock('next/font/google', () => ({
Inter: () => ({ className: 'font-inter', variable: '--font-inter' }),
}))

vi.mock('next/font/local', () => ({
default: () => ({ className: 'font-soehne', variable: '--font-soehne' }),
}))

vi.mock('@/lib/core/config/env', () => ({
getEnv: mockGetEnv,
isTruthy: (value: string | undefined) => value === 'true',
isFalsy: (value: string | undefined) => value === 'false',
env: {
NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: 'true',
},
}))

vi.mock('@/lib/core/config/feature-flags', () => ({
getAuthTermsLinkConfig: () => mockFeatureFlags.getAuthTermsLinkConfig(),
getAuthPrivacyLinkConfig: () => mockFeatureFlags.getAuthPrivacyLinkConfig(),
}))

vi.mock('@/lib/auth/auth-client', () => ({
client: {
signIn: { email: vi.fn(), social: vi.fn() },
signUp: { email: vi.fn() },
forgetPassword: vi.fn(),
},
useSession: () => ({ refetch: vi.fn() }),
}))

vi.mock('@/hooks/use-branded-button-class', () => ({
useBrandedButtonClass: () => 'brand-button',
}))

vi.mock('@/app/(auth)/components/branded-button', () => ({
BrandedButton: ({ children }: { children: React.ReactNode }) => <button>{children}</button>,
}))

vi.mock('@/app/(auth)/components/social-login-buttons', () => ({
SocialLoginButtons: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))

vi.mock('@/app/(auth)/components/sso-login-button', () => ({
SSOLoginButton: () => <button>SSO</button>,
}))

vi.mock('@/components/ui/dialog', () => ({
Dialog: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogDescription: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))

vi.mock('@/components/ui/input', () => ({
Input: (props: React.ComponentProps<'input'>) => <input {...props} />,
}))

vi.mock('@/components/ui/label', () => ({
Label: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))

vi.mock('@/components/ui/button', () => ({
Button: ({ children }: { children: React.ReactNode }) => <button>{children}</button>,
}))

vi.mock('@/lib/core/utils/cn', () => ({
cn: (...values: Array<string | undefined | false>) => values.filter(Boolean).join(' '),
}))

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

vi.mock('@/lib/messaging/email/validation', () => ({
quickValidateEmail: () => ({ isValid: true }),
}))

import SSOForm from '../../ee/sso/components/sso-form'
import LoginPage from './login/login-form'
import SignupPage from './signup/signup-form'

describe('auth legal link rendering', () => {
beforeEach(() => {
mockFeatureFlags.getAuthTermsLinkConfig = () => ({ href: '/terms', isExternal: false })
mockFeatureFlags.getAuthPrivacyLinkConfig = () => ({ href: '/privacy', isExternal: false })
})

it('renders internal legal links on auth surfaces when legal pages are enabled', () => {
const loginHtml = renderToStaticMarkup(
<LoginPage githubAvailable={false} googleAvailable={false} isProduction={false} />
)
const signupHtml = renderToStaticMarkup(
<SignupPage githubAvailable={false} googleAvailable={false} isProduction={false} />
)
const ssoHtml = renderToStaticMarkup(<SSOForm />)

expect(loginHtml).toContain('href="/terms"')
expect(loginHtml).toContain('href="/privacy"')
expect(signupHtml).toContain('href="/terms"')
expect(signupHtml).toContain('href="/privacy"')
expect(ssoHtml).toContain('href="/terms"')
expect(ssoHtml).toContain('href="/privacy"')
})

it('renders external legal links on auth surfaces when legal pages are disabled but external urls exist', () => {
mockFeatureFlags.getAuthTermsLinkConfig = () => ({
href: 'https://legal.example.com/terms',
isExternal: true,
})
mockFeatureFlags.getAuthPrivacyLinkConfig = () => ({
href: 'https://legal.example.com/privacy',
isExternal: true,
})

const loginHtml = renderToStaticMarkup(
<LoginPage githubAvailable={false} googleAvailable={false} isProduction={false} />
)

expect(loginHtml).toContain('href="https://legal.example.com/terms"')
expect(loginHtml).toContain('href="https://legal.example.com/privacy"')
})

it('hides only the missing individual legal link when no external fallback exists', () => {
mockFeatureFlags.getAuthTermsLinkConfig = () => null
mockFeatureFlags.getAuthPrivacyLinkConfig = () => ({
href: 'https://legal.example.com/privacy',
isExternal: true,
})

const loginHtml = renderToStaticMarkup(
<LoginPage githubAvailable={false} googleAvailable={false} isProduction={false} />
)

expect(loginHtml).not.toContain('Terms of Service</a>')
expect(loginHtml).toContain('Privacy Policy</a>')
expect(loginHtml).toContain('href="https://legal.example.com/privacy"')
})
})
51 changes: 30 additions & 21 deletions apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { client } from '@/lib/auth/auth-client'
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
import { getAuthPrivacyLinkConfig, getAuthTermsLinkConfig } from '@/lib/core/config/feature-flags'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
Expand Down Expand Up @@ -114,6 +115,8 @@ export default function LoginPage({
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('')
const [isSubmittingReset, setIsSubmittingReset] = useState(false)
const termsLinkConfig = getAuthTermsLinkConfig()
const privacyLinkConfig = getAuthPrivacyLinkConfig()
const [resetStatus, setResetStatus] = useState<{
type: 'success' | 'error' | null
message: string
Expand Down Expand Up @@ -548,28 +551,34 @@ export default function LoginPage({
</div>
)}

<div
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
>
By signing in, you agree to our{' '}
<Link
href='/terms'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
{(termsLinkConfig || privacyLinkConfig) && (
<div
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
>
Terms of Service
</Link>{' '}
and{' '}
<Link
href='/privacy'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
>
Privacy Policy
</Link>
</div>
By signing in, you agree to our{' '}
{termsLinkConfig ? (
<Link
href={termsLinkConfig.href}
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
>
Terms of Service
</Link>
) : null}
{termsLinkConfig && privacyLinkConfig ? ' and ' : null}
{privacyLinkConfig ? (
<Link
href={privacyLinkConfig.href}
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
>
Privacy Policy
</Link>
) : null}
</div>
)}

<Dialog open={forgotPasswordOpen} onOpenChange={setForgotPasswordOpen}>
<DialogContent className='auth-card auth-card-shadow max-w-[540px] rounded-[10px] border backdrop-blur-sm'>
Expand Down
51 changes: 30 additions & 21 deletions apps/sim/app/(auth)/signup/signup-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { client, useSession } from '@/lib/auth/auth-client'
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
import { getAuthPrivacyLinkConfig, getAuthTermsLinkConfig } from '@/lib/core/config/feature-flags'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
Expand Down Expand Up @@ -97,6 +98,8 @@ function SignupFormContent({
const [redirectUrl, setRedirectUrl] = useState('')
const [isInviteFlow, setIsInviteFlow] = useState(false)
const buttonClass = useBrandedButtonClass()
const termsLinkConfig = getAuthTermsLinkConfig()
const privacyLinkConfig = getAuthPrivacyLinkConfig()

const [name, setName] = useState('')
const [nameErrors, setNameErrors] = useState<string[]>([])
Expand Down Expand Up @@ -547,28 +550,34 @@ function SignupFormContent({
</Link>
</div>

<div
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
>
By creating an account, you agree to our{' '}
<Link
href='/terms'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
>
Terms of Service
</Link>{' '}
and{' '}
<Link
href='/privacy'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
{(termsLinkConfig || privacyLinkConfig) && (
<div
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
>
Privacy Policy
</Link>
</div>
By creating an account, you agree to our{' '}
{termsLinkConfig ? (
<Link
href={termsLinkConfig.href}
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
>
Terms of Service
</Link>
) : null}
{termsLinkConfig && privacyLinkConfig ? ' and ' : null}
{privacyLinkConfig ? (
<Link
href={privacyLinkConfig.href}
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
>
Privacy Policy
</Link>
) : null}
</div>
)}
</>
)
}
Expand Down
25 changes: 17 additions & 8 deletions apps/sim/app/(landing)/components/footer/components/logo.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import Image from 'next/image'
import Link from 'next/link'
import { isPublicLandingPageEnabled } from '@/lib/core/config/feature-flags'

export default function Logo() {
const logoImage = (
<Image
src='/logo/b&w/text/b&w.svg'
alt='Sim - Workflows for LLMs'
width={49.78314}
height={24.276}
priority
quality={90}
/>
)

if (!isPublicLandingPageEnabled) {
return <div aria-label='Sim home'>{logoImage}</div>
}

return (
<Link href='/' aria-label='Sim home'>
<Image
src='/logo/b&w/text/b&w.svg'
alt='Sim - Workflows for LLMs'
width={49.78314}
height={24.276}
priority
quality={90}
/>
{logoImage}
</Link>
)
}
Loading
Loading