fix(chat-subs): always use getBaseUrl helper to fetch base url (#1643)

* fix(chat-subs): always use next public app url env

* use getBaseUrl everywhere

* move remaining uses

* fix test

* change auth.ts and make getBaseUrl() call not top level for emails

* change remaining uses

* revert csp

* cleanup

* fix
This commit is contained in:
Vikhyath Mondreti
2025-10-15 14:13:23 -07:00
committed by GitHub
parent 4cceb22f21
commit eb4821ff30
46 changed files with 180 additions and 306 deletions

View File

@@ -18,6 +18,7 @@ import { client } from '@/lib/auth-client'
import { quickValidateEmail } from '@/lib/email/validation'
import { env, isFalsy, isTruthy } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { cn } from '@/lib/utils'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
@@ -322,7 +323,7 @@ export default function LoginPage({
},
body: JSON.stringify({
email: forgotPasswordEmail,
redirectTo: `${window.location.origin}/reset-password`,
redirectTo: `${getBaseUrl()}/reset-password`,
}),
})

View File

@@ -4,8 +4,8 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { requireStripeClient } from '@/lib/billing/stripe-client'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('BillingPortal')
@@ -21,8 +21,7 @@ export async function POST(request: NextRequest) {
const context: 'user' | 'organization' =
body?.context === 'organization' ? 'organization' : 'user'
const organizationId: string | undefined = body?.organizationId || undefined
const returnUrl: string =
body?.returnUrl || `${env.NEXT_PUBLIC_APP_URL}/workspace?billing=updated`
const returnUrl: string = body?.returnUrl || `${getBaseUrl()}/workspace?billing=updated`
const stripe = requireStripeClient()

View File

@@ -5,9 +5,9 @@ import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { encryptSecret } from '@/lib/utils'
import { checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -171,7 +171,7 @@ export async function POST(request: NextRequest) {
// Return successful response with chat URL
// Generate chat URL using path-based routing instead of subdomains
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
const baseUrl = getBaseUrl()
let chatUrl: string
try {

View File

@@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { getPresignedUrl, getPresignedUrlWithConfig, isUsingCloudStorage } from '@/lib/uploads'
import { BLOB_EXECUTION_FILES_CONFIG, S3_EXECUTION_FILES_CONFIG } from '@/lib/uploads/setup'
import { getBaseUrl } from '@/lib/urls/utils'
import { createErrorResponse } from '@/app/api/files/utils'
const logger = createLogger('FileDownload')
@@ -81,7 +82,7 @@ export async function POST(request: NextRequest) {
}
} else {
// For local storage, return the direct path
const downloadUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/files/serve/${key}`
const downloadUrl = `${getBaseUrl()}/api/files/serve/${key}`
return NextResponse.json({
downloadUrl,

View File

@@ -1,7 +1,6 @@
import { createContext, Script } from 'vm'
import { type NextRequest, NextResponse } from 'next/server'
import { env, isTruthy } from '@/lib/env'
import { MAX_EXECUTION_DURATION } from '@/lib/execution/constants'
import { executeInE2B } from '@/lib/execution/e2b'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { createLogger } from '@/lib/logs/console/logger'
@@ -9,7 +8,9 @@ import { validateProxyUrl } from '@/lib/security/input-validation'
import { generateRequestId } from '@/lib/utils'
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
export const maxDuration = MAX_EXECUTION_DURATION
// Segment config exports must be statically analyzable.
// Mirror MAX_EXECUTION_DURATION (210s) from '@/lib/execution/constants'.
export const maxDuration = 210
const logger = createLogger('FunctionExecuteAPI')

View File

@@ -23,9 +23,9 @@ import {
} from '@/lib/billing/validation/seat-management'
import { sendEmail } from '@/lib/email/mailer'
import { quickValidateEmail } from '@/lib/email/validation'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('OrganizationInvitations')
@@ -339,7 +339,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
organizationEntry[0]?.name || 'organization',
role,
workspaceInvitationsWithNames,
`${env.NEXT_PUBLIC_APP_URL}/invite/${orgInvitation.id}`
`${getBaseUrl()}/invite/${orgInvitation.id}`
)
emailResult = await sendEmail({
@@ -352,7 +352,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
`${env.NEXT_PUBLIC_APP_URL}/invite/${orgInvitation.id}`,
`${getBaseUrl()}/invite/${orgInvitation.id}`,
email
)

View File

@@ -9,8 +9,8 @@ import { getUserUsageData } from '@/lib/billing/core/usage'
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
import { sendEmail } from '@/lib/email/mailer'
import { quickValidateEmail } from '@/lib/email/validation'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('OrganizationMembersAPI')
@@ -260,7 +260,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
`${env.NEXT_PUBLIC_APP_URL}/invite/organization?id=${invitationId}`,
`${getBaseUrl()}/invite/organization?id=${invitationId}`,
normalizedEmail
)

View File

@@ -3,9 +3,9 @@ import { webhook, workflow } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
import { generateRequestId } from '@/lib/utils'
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
@@ -282,13 +282,7 @@ export async function DELETE(
if (!resolvedExternalId) {
try {
if (!env.NEXT_PUBLIC_APP_URL) {
logger.error(
`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot match Airtable webhook`
)
throw new Error('NEXT_PUBLIC_APP_URL must be configured')
}
const expectedNotificationUrl = `${env.NEXT_PUBLIC_APP_URL}/api/webhooks/trigger/${foundWebhook.path}`
const expectedNotificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${foundWebhook.path}`
const listUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`
const listResp = await fetch(listUrl, {

View File

@@ -2,9 +2,9 @@ import { db, webhook, workflow } from '@sim/db'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
import { generateRequestId } from '@/lib/utils'
import { signTestWebhookToken } from '@/lib/webhooks/test-tokens'
@@ -64,13 +64,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
if (!env.NEXT_PUBLIC_APP_URL) {
logger.error(`[${requestId}] NEXT_PUBLIC_APP_URL not configured`)
return NextResponse.json({ error: 'Server configuration error' }, { status: 500 })
}
const token = await signTestWebhookToken(id, ttlSeconds)
const url = `${env.NEXT_PUBLIC_APP_URL}/api/webhooks/test/${id}?token=${encodeURIComponent(token)}`
const url = `${getBaseUrl()}/api/webhooks/test/${id}?token=${encodeURIComponent(token)}`
logger.info(`[${requestId}] Minted test URL for webhook ${id}`)
return NextResponse.json({

View File

@@ -4,9 +4,9 @@ import { and, desc, eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
import { generateRequestId } from '@/lib/utils'
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
@@ -467,14 +467,7 @@ async function createAirtableWebhookSubscription(
)
}
if (!env.NEXT_PUBLIC_APP_URL) {
logger.error(
`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot register Airtable webhook`
)
throw new Error('NEXT_PUBLIC_APP_URL must be configured for Airtable webhook registration')
}
const notificationUrl = `${env.NEXT_PUBLIC_APP_URL}/api/webhooks/trigger/${path}`
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
const airtableApiUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`

View File

@@ -2,8 +2,8 @@ import { db } from '@sim/db'
import { webhook } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('WebhookTestAPI')
@@ -35,15 +35,7 @@ export async function GET(request: NextRequest) {
const provider = foundWebhook.provider || 'generic'
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
if (!env.NEXT_PUBLIC_APP_URL) {
logger.error(`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot test webhook`)
return NextResponse.json(
{ success: false, error: 'NEXT_PUBLIC_APP_URL must be configured' },
{ status: 500 }
)
}
const baseUrl = env.NEXT_PUBLIC_APP_URL
const webhookUrl = `${baseUrl}/api/webhooks/trigger/${foundWebhook.path}`
const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${foundWebhook.path}`
logger.info(`[${requestId}] Testing webhook for provider: ${provider}`, {
webhookId,

View File

@@ -61,17 +61,21 @@ describe('Workspace Invitation [invitationId] API Route', () => {
hasWorkspaceAdminAccess: mockHasWorkspaceAdminAccess,
}))
vi.doMock('@/lib/env', () => ({
env: {
vi.doMock('@/lib/env', () => {
const mockEnv = {
NEXT_PUBLIC_APP_URL: 'https://test.sim.ai',
BILLING_ENABLED: false,
},
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string'
? value.toLowerCase() === 'true' || value === '1'
: Boolean(value),
getEnv: (variable: string) => process.env[variable],
}))
}
return {
env: mockEnv,
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string'
? value.toLowerCase() === 'true' || value === '1'
: Boolean(value),
getEnv: (variable: string) =>
mockEnv[variable as keyof typeof mockEnv] ?? process.env[variable],
}
})
mockTransaction = vi.fn()
const mockDbChain = {
@@ -384,17 +388,21 @@ describe('Workspace Invitation [invitationId] API Route', () => {
vi.doMock('@/lib/permissions/utils', () => ({
hasWorkspaceAdminAccess: vi.fn(),
}))
vi.doMock('@/lib/env', () => ({
env: {
vi.doMock('@/lib/env', () => {
const mockEnv = {
NEXT_PUBLIC_APP_URL: 'https://test.sim.ai',
BILLING_ENABLED: false,
},
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string'
? value.toLowerCase() === 'true' || value === '1'
: Boolean(value),
getEnv: (variable: string) => process.env[variable],
}))
}
return {
env: mockEnv,
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string'
? value.toLowerCase() === 'true' || value === '1'
: Boolean(value),
getEnv: (variable: string) =>
mockEnv[variable as keyof typeof mockEnv] ?? process.env[variable],
}
})
vi.doMock('@sim/db/schema', () => ({
workspaceInvitation: { id: 'id' },
}))

View File

@@ -14,8 +14,8 @@ import { WorkspaceInvitationEmail } from '@/components/emails/workspace-invitati
import { getSession } from '@/lib/auth'
import { sendEmail } from '@/lib/email/mailer'
import { getFromEmailAddress } from '@/lib/email/utils'
import { env } from '@/lib/env'
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
// GET /api/workspaces/invitations/[invitationId] - Get invitation details OR accept via token
export async function GET(
@@ -30,12 +30,7 @@ export async function GET(
if (!session?.user?.id) {
// For token-based acceptance flows, redirect to login
if (isAcceptFlow) {
return NextResponse.redirect(
new URL(
`/invite/${invitationId}?token=${token}`,
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
)
return NextResponse.redirect(new URL(`/invite/${invitationId}?token=${token}`, getBaseUrl()))
}
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
@@ -54,10 +49,7 @@ export async function GET(
if (!invitation) {
if (isAcceptFlow) {
return NextResponse.redirect(
new URL(
`/invite/${invitationId}?error=invalid-token`,
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
new URL(`/invite/${invitationId}?error=invalid-token`, getBaseUrl())
)
}
return NextResponse.json({ error: 'Invitation not found or has expired' }, { status: 404 })
@@ -66,10 +58,7 @@ export async function GET(
if (new Date() > new Date(invitation.expiresAt)) {
if (isAcceptFlow) {
return NextResponse.redirect(
new URL(
`/invite/${invitation.id}?error=expired`,
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
new URL(`/invite/${invitation.id}?error=expired`, getBaseUrl())
)
}
return NextResponse.json({ error: 'Invitation has expired' }, { status: 400 })
@@ -84,10 +73,7 @@ export async function GET(
if (!workspaceDetails) {
if (isAcceptFlow) {
return NextResponse.redirect(
new URL(
`/invite/${invitation.id}?error=workspace-not-found`,
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
new URL(`/invite/${invitation.id}?error=workspace-not-found`, getBaseUrl())
)
}
return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
@@ -96,10 +82,7 @@ export async function GET(
if (isAcceptFlow) {
if (invitation.status !== ('pending' as WorkspaceInvitationStatus)) {
return NextResponse.redirect(
new URL(
`/invite/${invitation.id}?error=already-processed`,
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
new URL(`/invite/${invitation.id}?error=already-processed`, getBaseUrl())
)
}
@@ -114,10 +97,7 @@ export async function GET(
if (!userData) {
return NextResponse.redirect(
new URL(
`/invite/${invitation.id}?error=user-not-found`,
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
new URL(`/invite/${invitation.id}?error=user-not-found`, getBaseUrl())
)
}
@@ -125,10 +105,7 @@ export async function GET(
if (!isValidMatch) {
return NextResponse.redirect(
new URL(
`/invite/${invitation.id}?error=email-mismatch`,
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
new URL(`/invite/${invitation.id}?error=email-mismatch`, getBaseUrl())
)
}
@@ -154,10 +131,7 @@ export async function GET(
.where(eq(workspaceInvitation.id, invitation.id))
return NextResponse.redirect(
new URL(
`/workspace/${invitation.workspaceId}/w`,
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
new URL(`/workspace/${invitation.workspaceId}/w`, getBaseUrl())
)
}
@@ -181,12 +155,7 @@ export async function GET(
.where(eq(workspaceInvitation.id, invitation.id))
})
return NextResponse.redirect(
new URL(
`/workspace/${invitation.workspaceId}/w`,
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
)
return NextResponse.redirect(new URL(`/workspace/${invitation.workspaceId}/w`, getBaseUrl()))
}
return NextResponse.json({
@@ -298,7 +267,7 @@ export async function POST(
.set({ token: newToken, expiresAt: newExpiresAt, updatedAt: new Date() })
.where(eq(workspaceInvitation.id, invitationId))
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
const baseUrl = getBaseUrl()
const invitationLink = `${baseUrl}/invite/${invitationId}?token=${newToken}`
const emailHtml = await render(

View File

@@ -15,8 +15,8 @@ import { WorkspaceInvitationEmail } from '@/components/emails/workspace-invitati
import { getSession } from '@/lib/auth'
import { sendEmail } from '@/lib/email/mailer'
import { getFromEmailAddress } from '@/lib/email/utils'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
export const dynamic = 'force-dynamic'
@@ -232,7 +232,7 @@ async function sendInvitationEmail({
token: string
}) {
try {
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
const baseUrl = getBaseUrl()
// Use invitation ID in path, token in query parameter for security
const invitationLink = `${baseUrl}/invite/${invitationId}?token=${token}`

View File

@@ -20,6 +20,7 @@ import {
} from '@/components/ui/select'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { cn } from '@/lib/utils'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { getTrigger } from '@/triggers'
@@ -284,8 +285,7 @@ export function TriggerModal({
}
if (finalPath) {
const baseUrl = window.location.origin
setWebhookUrl(`${baseUrl}/api/webhooks/trigger/${finalPath}`)
setWebhookUrl(`${getBaseUrl()}/api/webhooks/trigger/${finalPath}`)
}
}, [
triggerPath,

View File

@@ -9,6 +9,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import {
AirtableConfig,
DeleteConfirmDialog,
@@ -404,12 +405,7 @@ export function WebhookModal({
}, [webhookPath])
// Construct the full webhook URL
const baseUrl =
typeof window !== 'undefined'
? `${window.location.protocol}//${window.location.host}`
: 'https://your-domain.com'
const webhookUrl = `${baseUrl}/api/webhooks/trigger/${formattedPath}`
const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${formattedPath}`
const generateTestUrl = async () => {
if (!webhookId) return

View File

@@ -12,6 +12,7 @@ import { Skeleton } from '@/components/ui/skeleton'
import { signOut, useSession } from '@/lib/auth-client'
import { useBrandConfig } from '@/lib/branding/branding'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/account/hooks/use-profile-picture-upload'
import { clearUserData } from '@/stores'
@@ -208,7 +209,7 @@ export function Account(_props: AccountProps) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
redirectTo: `${window.location.origin}/reset-password`,
redirectTo: `${getBaseUrl()}/reset-password`,
}),
})

View File

@@ -5,9 +5,9 @@ import { Check, ChevronDown, Copy, Eye, EyeOff } from 'lucide-react'
import { Alert, AlertDescription, Button, Input, Label } from '@/components/ui'
import { Skeleton } from '@/components/ui/skeleton'
import { useSession } from '@/lib/auth-client'
import { env } from '@/lib/env'
import { isBillingEnabled } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { cn } from '@/lib/utils'
import { useOrganizationStore } from '@/stores/organization'
@@ -441,7 +441,7 @@ export function SSO() {
})
}
const callbackUrl = `${env.NEXT_PUBLIC_APP_URL}/api/auth/sso/callback/${formData.providerId}`
const callbackUrl = `${getBaseUrl()}/api/auth/sso/callback/${formData.providerId}`
const copyCallback = async () => {
try {
@@ -551,14 +551,14 @@ export function SSO() {
<div className='relative mt-2'>
<Input
readOnly
value={`${env.NEXT_PUBLIC_APP_URL}/api/auth/sso/callback/${provider.providerId}`}
value={`${getBaseUrl()}/api/auth/sso/callback/${provider.providerId}`}
className='h-9 w-full cursor-text pr-10 font-mono text-xs focus-visible:ring-2 focus-visible:ring-primary/20'
onClick={(e) => (e.target as HTMLInputElement).select()}
/>
<button
type='button'
onClick={() => {
const url = `${env.NEXT_PUBLIC_APP_URL}/api/auth/sso/callback/${provider.providerId}`
const url = `${getBaseUrl()}/api/auth/sso/callback/${provider.providerId}`
navigator.clipboard.writeText(url)
setCopied(true)
setTimeout(() => setCopied(false), 1500)

View File

@@ -15,6 +15,7 @@ import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { useSession, useSubscription } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { cn } from '@/lib/utils'
import { useOrganizationStore } from '@/stores/organization'
import { useSubscriptionStore } from '@/stores/subscription/store'
@@ -89,7 +90,7 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
throw new Error('Subscription management not available')
}
const returnUrl = window.location.origin + window.location.pathname.split('/w/')[0]
const returnUrl = getBaseUrl() + window.location.pathname.split('/w/')[0]
const cancelParams: any = {
returnUrl,

View File

@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { Skeleton, Switch } from '@/components/ui'
import { useSession } from '@/lib/auth-client'
import { useSubscriptionUpgrade } from '@/lib/subscription/upgrade'
import { getBaseUrl } from '@/lib/urls/utils'
import { cn } from '@/lib/utils'
import { UsageHeader } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/shared/usage-header'
import {
@@ -391,7 +392,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
context:
subscription.isTeam || subscription.isEnterprise ? 'organization' : 'user',
organizationId: activeOrgId,
returnUrl: `${window.location.origin}/workspace?billing=updated`,
returnUrl: `${getBaseUrl()}/workspace?billing=updated`,
}),
})
const data = await res.json()

View File

@@ -4,6 +4,7 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Skeleton } from '@/components/ui/skeleton'
import { useActiveOrganization } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { UsageHeader } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/shared/usage-header'
import {
UsageLimit,
@@ -122,7 +123,7 @@ export function TeamUsage({ hasAdminAccess }: TeamUsageProps) {
body: JSON.stringify({
context: 'organization',
organizationId: activeOrg?.id,
returnUrl: `${window.location.origin}/workspace?billing=updated`,
returnUrl: `${getBaseUrl()}/workspace?billing=updated`,
}),
})
const data = await res.json()

View File

@@ -12,7 +12,7 @@ import {
Text,
} from '@react-email/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { env } from '@/lib/env'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -30,8 +30,6 @@ interface BatchInvitationEmailProps {
acceptUrl: string
}
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
const getPermissionLabel = (permission: string) => {
switch (permission) {
case 'admin':
@@ -64,6 +62,7 @@ export const BatchInvitationEmail = ({
acceptUrl,
}: BatchInvitationEmailProps) => {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
const hasWorkspaces = workspaceInvitations.length > 0
return (

View File

@@ -13,7 +13,7 @@ import {
} from '@react-email/components'
import { format } from 'date-fns'
import { getBrandConfig } from '@/lib/branding/branding'
import { getEnv } from '@/lib/env'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -24,15 +24,15 @@ interface EnterpriseSubscriptionEmailProps {
createdDate?: Date
}
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'https://sim.ai'
export const EnterpriseSubscriptionEmail = ({
userName = 'Valued User',
userEmail = '',
loginLink = `${baseUrl}/login`,
loginLink,
createdDate = new Date(),
}: EnterpriseSubscriptionEmailProps) => {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
const effectiveLoginLink = loginLink || `${baseUrl}/login`
return (
<Html>
@@ -75,7 +75,7 @@ export const EnterpriseSubscriptionEmail = ({
in and start exploring your new Enterprise features:
</Text>
<Link href={loginLink} style={{ textDecoration: 'none' }}>
<Link href={effectiveLoginLink} style={{ textDecoration: 'none' }}>
<Text style={baseStyles.button}>Access Your Enterprise Account</Text>
</Link>

View File

@@ -1,7 +1,7 @@
import { Container, Img, Link, Section, Text } from '@react-email/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getEnv } from '@/lib/env'
import { isHosted } from '@/lib/environment'
import { getBaseUrl } from '@/lib/urls/utils'
interface UnsubscribeOptions {
unsubscribeToken?: string
@@ -13,10 +13,7 @@ interface EmailFooterProps {
unsubscribe?: UnsubscribeOptions
}
export const EmailFooter = ({
baseUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'https://sim.ai',
unsubscribe,
}: EmailFooterProps) => {
export const EmailFooter = ({ baseUrl = getBaseUrl(), unsubscribe }: EmailFooterProps) => {
const brand = getBrandConfig()
return (

View File

@@ -12,7 +12,7 @@ import {
} from '@react-email/components'
import { format } from 'date-fns'
import { getBrandConfig } from '@/lib/branding/branding'
import { getEnv } from '@/lib/env'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -23,8 +23,6 @@ interface HelpConfirmationEmailProps {
submittedDate?: Date
}
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'https://sim.ai'
const getTypeLabel = (type: string) => {
switch (type) {
case 'bug':
@@ -47,6 +45,7 @@ export const HelpConfirmationEmail = ({
submittedDate = new Date(),
}: HelpConfirmationEmailProps) => {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
const typeLabel = getTypeLabel(type)
return (

View File

@@ -13,8 +13,8 @@ import {
} from '@react-email/components'
import { format } from 'date-fns'
import { getBrandConfig } from '@/lib/branding/branding'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -26,8 +26,6 @@ interface InvitationEmailProps {
updatedDate?: Date
}
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'https://sim.ai'
const logger = createLogger('InvitationEmail')
export const InvitationEmail = ({
@@ -38,6 +36,7 @@ export const InvitationEmail = ({
updatedDate = new Date(),
}: InvitationEmailProps) => {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
// Extract invitation ID or token from inviteLink if present
let enhancedLink = inviteLink

View File

@@ -11,7 +11,7 @@ import {
Text,
} from '@react-email/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getEnv } from '@/lib/env'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -22,8 +22,6 @@ interface OTPVerificationEmailProps {
chatTitle?: string
}
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'https://sim.ai'
const getSubjectByType = (type: string, brandName: string, chatTitle?: string) => {
switch (type) {
case 'sign-in':
@@ -46,6 +44,7 @@ export const OTPVerificationEmail = ({
chatTitle,
}: OTPVerificationEmailProps) => {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
// Get a message based on the type
const getMessage = () => {

View File

@@ -14,7 +14,7 @@ import {
} from '@react-email/components'
import EmailFooter from '@/components/emails/footer'
import { getBrandConfig } from '@/lib/branding/branding'
import { getEnv } from '@/lib/env'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
interface PlanWelcomeEmailProps {
@@ -31,7 +31,7 @@ export function PlanWelcomeEmail({
createdDate = new Date(),
}: PlanWelcomeEmailProps) {
const brand = getBrandConfig()
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'https://sim.ai'
const baseUrl = getBaseUrl()
const cta = loginLink || `${baseUrl}/login`
const previewText = `${brand.name}: Your ${planName} plan is active`

View File

@@ -10,6 +10,7 @@ import {
UsageThresholdEmail,
} from '@/components/emails'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/urls/utils'
export async function renderOTPEmail(
otp: string,
@@ -89,7 +90,7 @@ export async function renderEnterpriseSubscriptionEmail(
userName: string,
userEmail: string
): Promise<string> {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
const baseUrl = getBaseUrl()
const loginLink = `${baseUrl}/login`
return await render(

View File

@@ -13,7 +13,7 @@ import {
} from '@react-email/components'
import { format } from 'date-fns'
import { getBrandConfig } from '@/lib/branding/branding'
import { getEnv } from '@/lib/env'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -23,14 +23,13 @@ interface ResetPasswordEmailProps {
updatedDate?: Date
}
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'https://sim.ai'
export const ResetPasswordEmail = ({
username = '',
resetLink = '',
updatedDate = new Date(),
}: ResetPasswordEmailProps) => {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
return (
<Html>

View File

@@ -14,7 +14,7 @@ import {
} from '@react-email/components'
import EmailFooter from '@/components/emails/footer'
import { getBrandConfig } from '@/lib/branding/branding'
import { getEnv } from '@/lib/env'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
interface UsageThresholdEmailProps {
@@ -37,7 +37,7 @@ export function UsageThresholdEmail({
updatedDate = new Date(),
}: UsageThresholdEmailProps) {
const brand = getBrandConfig()
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'https://sim.ai'
const baseUrl = getBaseUrl()
const previewText = `${brand.name}: You're at ${percentUsed}% of your ${planName} monthly budget`

View File

@@ -12,8 +12,8 @@ import {
Text,
} from '@react-email/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -25,14 +25,13 @@ interface WorkspaceInvitationEmailProps {
invitationLink?: string
}
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'https://sim.ai'
export const WorkspaceInvitationEmail = ({
workspaceName = 'Workspace',
inviterName = 'Someone',
invitationLink = '',
}: WorkspaceInvitationEmailProps) => {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
// Extract token from the link to ensure we're using the correct format
let enhancedLink = invitationLink

View File

@@ -9,16 +9,13 @@ import {
} from 'better-auth/client/plugins'
import { createAuthClient } from 'better-auth/react'
import type { auth } from '@/lib/auth'
import { env, getEnv } from '@/lib/env'
import { env } from '@/lib/env'
import { isBillingEnabled } from '@/lib/environment'
import { SessionContext, type SessionHookResult } from '@/lib/session/session-context'
export function getBaseURL() {
return getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000'
}
import { getBaseUrl } from '@/lib/urls/utils'
export const client = createAuthClient({
baseURL: getBaseURL(),
baseURL: getBaseUrl(),
plugins: [
emailOTPClient(),
genericOAuthClient(),

View File

@@ -22,7 +22,6 @@ import {
renderOTPEmail,
renderPasswordResetEmail,
} from '@/components/emails/render-email'
import { getBaseURL } from '@/lib/auth-client'
import { sendPlanWelcomeEmail } from '@/lib/billing'
import { authorizeSubscriptionReference } from '@/lib/billing/authorization'
import { handleNewUser } from '@/lib/billing/core/usage'
@@ -44,6 +43,7 @@ import { quickValidateEmail } from '@/lib/email/validation'
import { env, isTruthy } from '@/lib/env'
import { isBillingEnabled, isEmailVerificationEnabled } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { SSO_TRUSTED_PROVIDERS } from './sso/consts'
const logger = createLogger('Auth')
@@ -60,9 +60,9 @@ if (validStripeKey) {
}
export const auth = betterAuth({
baseURL: getBaseURL(),
baseURL: getBaseUrl(),
trustedOrigins: [
env.NEXT_PUBLIC_APP_URL,
getBaseUrl(),
...(env.NEXT_PUBLIC_SOCKET_URL ? [env.NEXT_PUBLIC_SOCKET_URL] : []),
].filter(Boolean),
database: drizzleAdapter(db, {
@@ -319,7 +319,7 @@ export const auth = betterAuth({
tokenUrl: 'https://github.com/login/oauth/access_token',
userInfoUrl: 'https://api.github.com/user',
scopes: ['user:email', 'repo', 'read:user', 'workflow'],
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/github-repo`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/github-repo`,
getUserInfo: async (tokens) => {
try {
const profileResponse = await fetch('https://api.github.com/user', {
@@ -400,7 +400,7 @@ export const auth = betterAuth({
'https://www.googleapis.com/auth/gmail.labels',
],
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-email`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-email`,
},
{
providerId: 'google-calendar',
@@ -414,7 +414,7 @@ export const auth = betterAuth({
'https://www.googleapis.com/auth/calendar',
],
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-calendar`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-calendar`,
},
{
providerId: 'google-drive',
@@ -428,7 +428,7 @@ export const auth = betterAuth({
'https://www.googleapis.com/auth/drive.file',
],
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-drive`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-drive`,
},
{
providerId: 'google-docs',
@@ -442,7 +442,7 @@ export const auth = betterAuth({
'https://www.googleapis.com/auth/drive.file',
],
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-docs`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-docs`,
},
{
providerId: 'google-sheets',
@@ -456,7 +456,7 @@ export const auth = betterAuth({
'https://www.googleapis.com/auth/drive.file',
],
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-sheets`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-sheets`,
},
{
@@ -471,7 +471,7 @@ export const auth = betterAuth({
'https://www.googleapis.com/auth/forms.responses.readonly',
],
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-forms`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-forms`,
},
{
@@ -487,7 +487,7 @@ export const auth = betterAuth({
'https://www.googleapis.com/auth/devstorage.read_only',
],
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-vault`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-vault`,
},
{
@@ -517,7 +517,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/microsoft-teams`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/microsoft-teams`,
},
{
@@ -532,7 +532,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/microsoft-excel`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/microsoft-excel`,
},
{
providerId: 'microsoft-planner',
@@ -554,7 +554,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/microsoft-planner`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/microsoft-planner`,
},
{
@@ -578,7 +578,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/outlook`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/outlook`,
},
{
@@ -593,7 +593,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/onedrive`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/onedrive`,
},
{
@@ -616,7 +616,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/sharepoint`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/sharepoint`,
},
{
@@ -628,7 +628,7 @@ export const auth = betterAuth({
userInfoUrl: 'https://dummy-not-used.wealthbox.com', // Dummy URL since no user info endpoint exists
scopes: ['login', 'data'],
responseType: 'code',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/wealthbox`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/wealthbox`,
getUserInfo: async (tokens) => {
try {
logger.info('Creating Wealthbox user profile from token data')
@@ -662,7 +662,7 @@ export const auth = betterAuth({
scopes: ['database.read', 'database.write', 'projects.read'],
responseType: 'code',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/supabase`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/supabase`,
getUserInfo: async (tokens) => {
try {
logger.info('Creating Supabase user profile from token data')
@@ -715,7 +715,7 @@ export const auth = betterAuth({
responseType: 'code',
prompt: 'consent',
authentication: 'basic',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/x`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/x`,
getUserInfo: async (tokens) => {
try {
const response = await fetch(
@@ -774,7 +774,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/confluence`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/confluence`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://api.atlassian.com/me', {
@@ -824,7 +824,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/discord`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/discord`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://discord.com/api/users/@me', {
@@ -895,7 +895,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/jira`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/jira`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://api.atlassian.com/me', {
@@ -946,7 +946,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/airtable`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/airtable`,
},
// Notion provider
@@ -963,7 +963,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/notion`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/notion`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://api.notion.com/v1/users/me', {
@@ -1013,7 +1013,7 @@ export const auth = betterAuth({
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/reddit`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/reddit`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://oauth.reddit.com/api/v1/me', {
@@ -1058,7 +1058,7 @@ export const auth = betterAuth({
tokenUrl: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
responseType: 'code',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/linear`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/linear`,
pkce: true,
prompt: 'consent',
accessType: 'offline',
@@ -1145,7 +1145,7 @@ export const auth = betterAuth({
responseType: 'code',
accessType: 'offline',
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/slack`,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/slack`,
getUserInfo: async (tokens) => {
try {
logger.info('Creating Slack bot profile from token data')
@@ -1413,7 +1413,7 @@ export const auth = betterAuth({
try {
const { invitation, organization, inviter } = data
const inviteUrl = `${env.NEXT_PUBLIC_APP_URL}/invite/${invitation.id}`
const inviteUrl = `${getBaseUrl()}/invite/${invitation.id}`
const inviterName = inviter.user?.name || 'A team member'
const html = await renderInvitationEmail(

View File

@@ -9,9 +9,9 @@ import {
getPerUserMinimumLimit,
} from '@/lib/billing/subscriptions/utils'
import type { UserSubscriptionState } from '@/lib/billing/types'
import { env } from '@/lib/env'
import { isProd } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('SubscriptionCore')
@@ -303,7 +303,7 @@ export async function sendPlanWelcomeEmail(subscription: any): Promise<void> {
)
const { sendEmail } = await import('@/lib/email/mailer')
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
const baseUrl = getBaseUrl()
const html = await renderPlanWelcomeEmail({
planName: subPlan === 'pro' ? 'Pro' : 'Team',
userName: users[0].name || undefined,

View File

@@ -13,6 +13,7 @@ import { sendEmail } from '@/lib/email/mailer'
import { getEmailPreferences } from '@/lib/email/unsubscribe'
import { isBillingEnabled } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('UsageManagement')
@@ -617,7 +618,7 @@ export async function maybeSendUsageThresholdEmail(params: {
if (!(params.percentBefore < 80 && params.percentAfter >= 80)) return
if (params.limit <= 0 || params.currentUsageAfter <= 0) return
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
const baseUrl = getBaseUrl()
const ctaLink = `${baseUrl}/workspace?billing=usage`
const sendTo = async (email: string, name?: string) => {
const prefs = await getEmailPreferences(email)

View File

@@ -1,6 +1,6 @@
import type { Metadata } from 'next'
import { getBrandConfig } from '@/lib/branding/branding'
import { env } from '@/lib/env'
import { getBaseUrl } from '@/lib/urls/utils'
/**
* Generate dynamic metadata based on brand configuration
@@ -40,9 +40,7 @@ export function generateBrandedMetadata(override: Partial<Metadata> = {}): Metad
referrer: 'origin-when-cross-origin',
creator: brand.name,
publisher: brand.name,
metadataBase: env.NEXT_PUBLIC_APP_URL
? new URL(env.NEXT_PUBLIC_APP_URL)
: new URL('https://sim.ai'),
metadataBase: new URL(getBaseUrl()),
alternates: {
canonical: '/',
languages: {
@@ -63,7 +61,7 @@ export function generateBrandedMetadata(override: Partial<Metadata> = {}): Metad
openGraph: {
type: 'website',
locale: 'en_US',
url: env.NEXT_PUBLIC_APP_URL || 'https://sim.ai',
url: getBaseUrl(),
title: defaultTitle,
description: summaryFull,
siteName: brand.name,

View File

@@ -43,6 +43,7 @@ vi.mock('@/lib/env', () => ({
vi.mock('@/lib/urls/utils', () => ({
getEmailDomain: vi.fn().mockReturnValue('sim.ai'),
getBaseUrl: vi.fn().mockReturnValue('https://test.sim.ai'),
}))
import { type EmailType, sendBatchEmails, sendEmail } from '@/lib/email/mailer'

View File

@@ -4,6 +4,7 @@ import { generateUnsubscribeToken, isUnsubscribed } from '@/lib/email/unsubscrib
import { getFromEmailAddress } from '@/lib/email/utils'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('Mailer')
@@ -167,7 +168,7 @@ async function processEmailData(options: EmailOptions): Promise<ProcessedEmailDa
// For arrays, use the first email for unsubscribe (batch emails typically go to similar recipients)
const primaryEmail = Array.isArray(to) ? to[0] : to
const unsubscribeToken = generateUnsubscribeToken(primaryEmail, emailType)
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
const baseUrl = getBaseUrl()
const unsubscribeUrl = `${baseUrl}/unsubscribe?token=${unsubscribeToken}&email=${encodeURIComponent(primaryEmail)}`
headers['List-Unsubscribe'] = `<${unsubscribeUrl}>`

View File

@@ -1,5 +1,5 @@
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { executeProviderRequest } from '@/providers'
import { getApiKey, getProviderFromModel } from '@/providers/utils'
@@ -41,7 +41,7 @@ async function queryKnowledgeBase(
})
// Call the knowledge base search API directly
const searchUrl = `${env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/knowledge/search`
const searchUrl = `${getBaseUrl()}/api/knowledge/search`
const response = await fetch(searchUrl, {
method: 'POST',

View File

@@ -2,25 +2,26 @@ import { getEnv } from '@/lib/env'
import { isProd } from '@/lib/environment'
/**
* Returns the base URL of the application, respecting environment variables for deployment environments
* Returns the base URL of the application from NEXT_PUBLIC_APP_URL
* This ensures webhooks, callbacks, and other integrations always use the correct public URL
* @returns The base URL string (e.g., 'http://localhost:3000' or 'https://example.com')
* @throws Error if NEXT_PUBLIC_APP_URL is not configured
*/
export function getBaseUrl(): string {
if (typeof window !== 'undefined' && window.location?.origin) {
return window.location.origin
}
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')
if (baseUrl) {
if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {
return baseUrl
}
const protocol = isProd ? 'https://' : 'http://'
return `${protocol}${baseUrl}`
if (!baseUrl) {
throw new Error(
'NEXT_PUBLIC_APP_URL must be configured for webhooks and callbacks to work correctly'
)
}
return 'http://localhost:3000'
if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {
return baseUrl
}
const protocol = isProd ? 'https://' : 'http://'
return `${protocol}${baseUrl}`
}
/**

View File

@@ -2,8 +2,8 @@ import { db } from '@sim/db'
import { webhook as webhookTable } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const teamsLogger = createLogger('TeamsSubscription')
@@ -71,11 +71,8 @@ export async function createTeamsSubscription(
}
// Build notification URL
const requestOrigin = new URL(request.url).origin
const effectiveOrigin = requestOrigin.includes('localhost')
? env.NEXT_PUBLIC_APP_URL || requestOrigin
: requestOrigin
const notificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${webhook.path}`
// Always use NEXT_PUBLIC_APP_URL to ensure Microsoft Graph can reach the public endpoint
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhook.path}`
// Subscribe to the specified chat
const resource = `/chats/${chatId}/messages`
@@ -221,14 +218,7 @@ export async function createTelegramWebhook(
return false
}
if (!env.NEXT_PUBLIC_APP_URL) {
telegramLogger.error(
`[${requestId}] NEXT_PUBLIC_APP_URL not configured, cannot register Telegram webhook`
)
return false
}
const notificationUrl = `${env.NEXT_PUBLIC_APP_URL}/api/webhooks/trigger/${webhook.path}`
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhook.path}`
const telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook`
const telegramResponse = await fetch(telegramApiUrl, {

View File

@@ -1,18 +1,12 @@
import { db } from '@sim/db'
import {
apiKey,
permissions,
userStats,
workflow as workflowTable,
workspace,
} from '@sim/db/schema'
import { apiKey, permissions, workflow as workflowTable, workspace } from '@sim/db/schema'
import type { InferSelectModel } from 'drizzle-orm'
import { and, eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import type { PermissionType } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
import type { ExecutionResult } from '@/executor/types'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
@@ -178,61 +172,19 @@ export async function updateWorkflowRunCounts(workflowId: string, runs = 1) {
throw new Error(`Workflow ${workflowId} not found`)
}
// Get the origin from the environment or use direct DB update as fallback
const origin =
getEnv('NEXT_PUBLIC_APP_URL') || (typeof window !== 'undefined' ? window.location.origin : '')
// Use the API to update stats
const response = await fetch(`${getBaseUrl()}/api/workflows/${workflowId}/stats?runs=${runs}`, {
method: 'POST',
})
if (origin) {
// Use absolute URL with origin
const response = await fetch(`${origin}/api/workflows/${workflowId}/stats?runs=${runs}`, {
method: 'POST',
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to update workflow stats')
}
return response.json()
}
logger.warn('No origin available, updating workflow stats directly via DB')
// Update workflow directly through database
await db
.update(workflowTable)
.set({
runCount: (workflow.runCount as number) + runs,
lastRunAt: new Date(),
})
.where(eq(workflowTable.id, workflowId))
// Update user stats if needed
if (workflow.userId) {
const userStatsRecord = await db
.select()
.from(userStats)
.where(eq(userStats.userId, workflow.userId))
.limit(1)
if (userStatsRecord.length === 0) {
console.warn('User stats record not found - should be created during onboarding', {
userId: workflow.userId,
})
return // Skip stats update if record doesn't exist
}
// Update existing record
await db
.update(userStats)
.set({
totalManualExecutions: userStatsRecord[0].totalManualExecutions + runs,
lastActive: new Date(),
})
.where(eq(userStats.userId, workflow.userId))
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to update workflow stats')
}
return { success: true, runsAdded: runs }
return response.json()
} catch (error) {
logger.error('Error updating workflow run counts:', error)
logger.error(`Error updating workflow stats for ${workflowId}`, error)
throw error
}
}

View File

@@ -3,6 +3,7 @@ import { Server } from 'socket.io'
import { env } from '@/lib/env'
import { isProd } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('SocketIOConfig')
@@ -11,7 +12,7 @@ const logger = createLogger('SocketIOConfig')
*/
function getAllowedOrigins(): string[] {
const allowedOrigins = [
env.NEXT_PUBLIC_APP_URL,
getBaseUrl(),
'http://localhost:3000',
'http://localhost:3001',
...(env.ALLOWED_ORIGINS?.split(',') || []),

View File

@@ -18,7 +18,7 @@ describe('HTTP Request Tool', () => {
beforeEach(() => {
tester = new ToolTester(requestTool)
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
process.env.NEXT_PUBLIC_APP_URL = 'https://app.simstudio.dev'
})
afterEach(() => {

View File

@@ -1,4 +1,3 @@
import { getEnv } from '@/lib/env'
import { isTest } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
@@ -6,18 +5,6 @@ import type { TableRow } from '@/tools/types'
const logger = createLogger('HTTPRequestUtils')
export const getReferer = (): string => {
if (typeof window !== 'undefined') {
return window.location.origin
}
try {
return getBaseUrl()
} catch (_error) {
return getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000'
}
}
/**
* Creates a set of default headers used in HTTP requests
* @param customHeaders Additional user-provided headers to include
@@ -35,7 +22,7 @@ export const getDefaultHeaders = (
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
Referer: getReferer(),
Referer: getBaseUrl(),
'Sec-Ch-Ua': 'Chromium;v=91, Not-A.Brand;v=99',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',