mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
committed by
GitHub
parent
4cceb22f21
commit
eb4821ff30
@@ -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`,
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' },
|
||||
}))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}`
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}>`
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(',') || []),
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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"',
|
||||
|
||||
Reference in New Issue
Block a user