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 { quickValidateEmail } from '@/lib/email/validation'
import { env, isFalsy, isTruthy } from '@/lib/env' import { env, isFalsy, isTruthy } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
@@ -322,7 +323,7 @@ export default function LoginPage({
}, },
body: JSON.stringify({ body: JSON.stringify({
email: forgotPasswordEmail, 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 { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
import { requireStripeClient } from '@/lib/billing/stripe-client' import { requireStripeClient } from '@/lib/billing/stripe-client'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('BillingPortal') const logger = createLogger('BillingPortal')
@@ -21,8 +21,7 @@ export async function POST(request: NextRequest) {
const context: 'user' | 'organization' = const context: 'user' | 'organization' =
body?.context === 'organization' ? 'organization' : 'user' body?.context === 'organization' ? 'organization' : 'user'
const organizationId: string | undefined = body?.organizationId || undefined const organizationId: string | undefined = body?.organizationId || undefined
const returnUrl: string = const returnUrl: string = body?.returnUrl || `${getBaseUrl()}/workspace?billing=updated`
body?.returnUrl || `${env.NEXT_PUBLIC_APP_URL}/workspace?billing=updated`
const stripe = requireStripeClient() const stripe = requireStripeClient()

View File

@@ -5,9 +5,9 @@ import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod' import { z } from 'zod'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { isDev } from '@/lib/environment' import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { encryptSecret } from '@/lib/utils' import { encryptSecret } from '@/lib/utils'
import { checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils' import { checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/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 // Return successful response with chat URL
// Generate chat URL using path-based routing instead of subdomains // 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 let chatUrl: string
try { try {

View File

@@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getPresignedUrl, getPresignedUrlWithConfig, isUsingCloudStorage } from '@/lib/uploads' import { getPresignedUrl, getPresignedUrlWithConfig, isUsingCloudStorage } from '@/lib/uploads'
import { BLOB_EXECUTION_FILES_CONFIG, S3_EXECUTION_FILES_CONFIG } from '@/lib/uploads/setup' 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' import { createErrorResponse } from '@/app/api/files/utils'
const logger = createLogger('FileDownload') const logger = createLogger('FileDownload')
@@ -81,7 +82,7 @@ export async function POST(request: NextRequest) {
} }
} else { } else {
// For local storage, return the direct path // 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({ return NextResponse.json({
downloadUrl, downloadUrl,

View File

@@ -1,7 +1,6 @@
import { createContext, Script } from 'vm' import { createContext, Script } from 'vm'
import { type NextRequest, NextResponse } from 'next/server' import { type NextRequest, NextResponse } from 'next/server'
import { env, isTruthy } from '@/lib/env' import { env, isTruthy } from '@/lib/env'
import { MAX_EXECUTION_DURATION } from '@/lib/execution/constants'
import { executeInE2B } from '@/lib/execution/e2b' import { executeInE2B } from '@/lib/execution/e2b'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages' import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
@@ -9,7 +8,9 @@ import { validateProxyUrl } from '@/lib/security/input-validation'
import { generateRequestId } from '@/lib/utils' import { generateRequestId } from '@/lib/utils'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
export const runtime = 'nodejs' 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') const logger = createLogger('FunctionExecuteAPI')

View File

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

View File

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

View File

@@ -3,9 +3,9 @@ import { webhook, workflow } from '@sim/db/schema'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server' import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils' import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
import { generateRequestId } from '@/lib/utils' import { generateRequestId } from '@/lib/utils'
import { getOAuthToken } from '@/app/api/auth/oauth/utils' import { getOAuthToken } from '@/app/api/auth/oauth/utils'
@@ -282,13 +282,7 @@ export async function DELETE(
if (!resolvedExternalId) { if (!resolvedExternalId) {
try { try {
if (!env.NEXT_PUBLIC_APP_URL) { const expectedNotificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${foundWebhook.path}`
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 listUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks` const listUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`
const listResp = await fetch(listUrl, { const listResp = await fetch(listUrl, {

View File

@@ -2,9 +2,9 @@ import { db, webhook, workflow } from '@sim/db'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server' import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils' import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
import { generateRequestId } from '@/lib/utils' import { generateRequestId } from '@/lib/utils'
import { signTestWebhookToken } from '@/lib/webhooks/test-tokens' 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 }) 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 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}`) logger.info(`[${requestId}] Minted test URL for webhook ${id}`)
return NextResponse.json({ return NextResponse.json({

View File

@@ -4,9 +4,9 @@ import { and, desc, eq } from 'drizzle-orm'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { type NextRequest, NextResponse } from 'next/server' import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils' import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
import { generateRequestId } from '@/lib/utils' import { generateRequestId } from '@/lib/utils'
import { getOAuthToken } from '@/app/api/auth/oauth/utils' import { getOAuthToken } from '@/app/api/auth/oauth/utils'
@@ -467,14 +467,7 @@ async function createAirtableWebhookSubscription(
) )
} }
if (!env.NEXT_PUBLIC_APP_URL) { const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
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 airtableApiUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks` 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 { webhook } from '@sim/db/schema'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server' import { type NextRequest, NextResponse } from 'next/server'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { generateRequestId } from '@/lib/utils' import { generateRequestId } from '@/lib/utils'
const logger = createLogger('WebhookTestAPI') const logger = createLogger('WebhookTestAPI')
@@ -35,15 +35,7 @@ export async function GET(request: NextRequest) {
const provider = foundWebhook.provider || 'generic' const provider = foundWebhook.provider || 'generic'
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {} const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
if (!env.NEXT_PUBLIC_APP_URL) { const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${foundWebhook.path}`
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}`
logger.info(`[${requestId}] Testing webhook for provider: ${provider}`, { logger.info(`[${requestId}] Testing webhook for provider: ${provider}`, {
webhookId, webhookId,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import { Skeleton } from '@/components/ui/skeleton'
import { signOut, useSession } from '@/lib/auth-client' import { signOut, useSession } from '@/lib/auth-client'
import { useBrandConfig } from '@/lib/branding/branding' import { useBrandConfig } from '@/lib/branding/branding'
import { createLogger } from '@/lib/logs/console/logger' 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 { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/account/hooks/use-profile-picture-upload'
import { clearUserData } from '@/stores' import { clearUserData } from '@/stores'
@@ -208,7 +209,7 @@ export function Account(_props: AccountProps) {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
email, 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 { Alert, AlertDescription, Button, Input, Label } from '@/components/ui'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useSession } from '@/lib/auth-client' import { useSession } from '@/lib/auth-client'
import { env } from '@/lib/env'
import { isBillingEnabled } from '@/lib/environment' import { isBillingEnabled } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useOrganizationStore } from '@/stores/organization' 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 () => { const copyCallback = async () => {
try { try {
@@ -551,14 +551,14 @@ export function SSO() {
<div className='relative mt-2'> <div className='relative mt-2'>
<Input <Input
readOnly 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' 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()} onClick={(e) => (e.target as HTMLInputElement).select()}
/> />
<button <button
type='button' type='button'
onClick={() => { 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) navigator.clipboard.writeText(url)
setCopied(true) setCopied(true)
setTimeout(() => setCopied(false), 1500) 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 { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { useSession, useSubscription } from '@/lib/auth-client' import { useSession, useSubscription } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useOrganizationStore } from '@/stores/organization' import { useOrganizationStore } from '@/stores/organization'
import { useSubscriptionStore } from '@/stores/subscription/store' import { useSubscriptionStore } from '@/stores/subscription/store'
@@ -89,7 +90,7 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
throw new Error('Subscription management not available') 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 = { const cancelParams: any = {
returnUrl, returnUrl,

View File

@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { Skeleton, Switch } from '@/components/ui' import { Skeleton, Switch } from '@/components/ui'
import { useSession } from '@/lib/auth-client' import { useSession } from '@/lib/auth-client'
import { useSubscriptionUpgrade } from '@/lib/subscription/upgrade' import { useSubscriptionUpgrade } from '@/lib/subscription/upgrade'
import { getBaseUrl } from '@/lib/urls/utils'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { UsageHeader } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/shared/usage-header' import { UsageHeader } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/shared/usage-header'
import { import {
@@ -391,7 +392,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
context: context:
subscription.isTeam || subscription.isEnterprise ? 'organization' : 'user', subscription.isTeam || subscription.isEnterprise ? 'organization' : 'user',
organizationId: activeOrgId, organizationId: activeOrgId,
returnUrl: `${window.location.origin}/workspace?billing=updated`, returnUrl: `${getBaseUrl()}/workspace?billing=updated`,
}), }),
}) })
const data = await res.json() 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 { Skeleton } from '@/components/ui/skeleton'
import { useActiveOrganization } from '@/lib/auth-client' import { useActiveOrganization } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger' 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 { UsageHeader } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/shared/usage-header'
import { import {
UsageLimit, UsageLimit,
@@ -122,7 +123,7 @@ export function TeamUsage({ hasAdminAccess }: TeamUsageProps) {
body: JSON.stringify({ body: JSON.stringify({
context: 'organization', context: 'organization',
organizationId: activeOrg?.id, organizationId: activeOrg?.id,
returnUrl: `${window.location.origin}/workspace?billing=updated`, returnUrl: `${getBaseUrl()}/workspace?billing=updated`,
}), }),
}) })
const data = await res.json() const data = await res.json()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,6 +43,7 @@ vi.mock('@/lib/env', () => ({
vi.mock('@/lib/urls/utils', () => ({ vi.mock('@/lib/urls/utils', () => ({
getEmailDomain: vi.fn().mockReturnValue('sim.ai'), getEmailDomain: vi.fn().mockReturnValue('sim.ai'),
getBaseUrl: vi.fn().mockReturnValue('https://test.sim.ai'),
})) }))
import { type EmailType, sendBatchEmails, sendEmail } from '@/lib/email/mailer' 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 { getFromEmailAddress } from '@/lib/email/utils'
import { env } from '@/lib/env' import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('Mailer') 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) // For arrays, use the first email for unsubscribe (batch emails typically go to similar recipients)
const primaryEmail = Array.isArray(to) ? to[0] : to const primaryEmail = Array.isArray(to) ? to[0] : to
const unsubscribeToken = generateUnsubscribeToken(primaryEmail, emailType) 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)}` const unsubscribeUrl = `${baseUrl}/unsubscribe?token=${unsubscribeToken}&email=${encodeURIComponent(primaryEmail)}`
headers['List-Unsubscribe'] = `<${unsubscribeUrl}>` headers['List-Unsubscribe'] = `<${unsubscribeUrl}>`

View File

@@ -1,5 +1,5 @@
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { executeProviderRequest } from '@/providers' import { executeProviderRequest } from '@/providers'
import { getApiKey, getProviderFromModel } from '@/providers/utils' import { getApiKey, getProviderFromModel } from '@/providers/utils'
@@ -41,7 +41,7 @@ async function queryKnowledgeBase(
}) })
// Call the knowledge base search API directly // 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, { const response = await fetch(searchUrl, {
method: 'POST', method: 'POST',

View File

@@ -2,25 +2,26 @@ import { getEnv } from '@/lib/env'
import { isProd } from '@/lib/environment' 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') * @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 { export function getBaseUrl(): string {
if (typeof window !== 'undefined' && window.location?.origin) {
return window.location.origin
}
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')
if (baseUrl) {
if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {
return baseUrl
}
const protocol = isProd ? 'https://' : 'http://' if (!baseUrl) {
return `${protocol}${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 { webhook as webhookTable } from '@sim/db/schema'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server' import type { NextRequest } from 'next/server'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const teamsLogger = createLogger('TeamsSubscription') const teamsLogger = createLogger('TeamsSubscription')
@@ -71,11 +71,8 @@ export async function createTeamsSubscription(
} }
// Build notification URL // Build notification URL
const requestOrigin = new URL(request.url).origin // Always use NEXT_PUBLIC_APP_URL to ensure Microsoft Graph can reach the public endpoint
const effectiveOrigin = requestOrigin.includes('localhost') const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhook.path}`
? env.NEXT_PUBLIC_APP_URL || requestOrigin
: requestOrigin
const notificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${webhook.path}`
// Subscribe to the specified chat // Subscribe to the specified chat
const resource = `/chats/${chatId}/messages` const resource = `/chats/${chatId}/messages`
@@ -221,14 +218,7 @@ export async function createTelegramWebhook(
return false return false
} }
if (!env.NEXT_PUBLIC_APP_URL) { const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhook.path}`
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 telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook` const telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook`
const telegramResponse = await fetch(telegramApiUrl, { const telegramResponse = await fetch(telegramApiUrl, {

View File

@@ -1,18 +1,12 @@
import { db } from '@sim/db' import { db } from '@sim/db'
import { import { apiKey, permissions, workflow as workflowTable, workspace } from '@sim/db/schema'
apiKey,
permissions,
userStats,
workflow as workflowTable,
workspace,
} from '@sim/db/schema'
import type { InferSelectModel } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm'
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import type { PermissionType } from '@/lib/permissions/utils' import type { PermissionType } from '@/lib/permissions/utils'
import { getBaseUrl } from '@/lib/urls/utils'
import type { ExecutionResult } from '@/executor/types' import type { ExecutionResult } from '@/executor/types'
import type { WorkflowState } from '@/stores/workflows/workflow/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`) throw new Error(`Workflow ${workflowId} not found`)
} }
// Get the origin from the environment or use direct DB update as fallback // Use the API to update stats
const origin = const response = await fetch(`${getBaseUrl()}/api/workflows/${workflowId}/stats?runs=${runs}`, {
getEnv('NEXT_PUBLIC_APP_URL') || (typeof window !== 'undefined' ? window.location.origin : '') method: 'POST',
})
if (origin) { if (!response.ok) {
// Use absolute URL with origin const error = await response.json()
const response = await fetch(`${origin}/api/workflows/${workflowId}/stats?runs=${runs}`, { throw new Error(error.error || 'Failed to update workflow stats')
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))
} }
return { success: true, runsAdded: runs } return response.json()
} catch (error) { } catch (error) {
logger.error('Error updating workflow run counts:', error) logger.error(`Error updating workflow stats for ${workflowId}`, error)
throw error throw error
} }
} }

View File

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

View File

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

View File

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