mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 07:27:57 -05: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 { 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`,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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' },
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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}>`
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(',') || []),
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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"',
|
||||||
|
|||||||
Reference in New Issue
Block a user