mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-25 06:48:12 -05:00
Compare commits
57 Commits
v0.5.66
...
fix/ci-com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c1d5586d5 | ||
|
|
bcf6dc8828 | ||
|
|
841cb638fb | ||
|
|
c7db48e3a2 | ||
|
|
4d844651c2 | ||
|
|
9f916940b3 | ||
|
|
3bbf7f5d1d | ||
|
|
68683258c3 | ||
|
|
fc7f56e21b | ||
|
|
8429040921 | ||
|
|
8574e6c71f | ||
|
|
9c3e663cd8 | ||
|
|
48adaa00d8 | ||
|
|
211a7ac3a4 | ||
|
|
0f9b6ad1d2 | ||
|
|
12100e6881 | ||
|
|
23294683e1 | ||
|
|
b913cff46e | ||
|
|
428781ce7d | ||
|
|
f0ee67f3ed | ||
|
|
f44594c380 | ||
|
|
6464cfa7f2 | ||
|
|
7f4edc85ef | ||
|
|
efef91ece0 | ||
|
|
64efeaa2e6 | ||
|
|
9b72b52b33 | ||
|
|
1467862488 | ||
|
|
7f2262857c | ||
|
|
1b309b50e6 | ||
|
|
f765b83a26 | ||
|
|
aa99db6fdd | ||
|
|
748793e07d | ||
|
|
91da7e183a | ||
|
|
ab09a5ad23 | ||
|
|
fcd0240db6 | ||
|
|
4e4149792a | ||
|
|
9a8b591257 | ||
|
|
f3ae3f8442 | ||
|
|
66dfe2c6b2 | ||
|
|
376f7cb571 | ||
|
|
42159c23b9 | ||
|
|
2f0f246002 | ||
|
|
900d3ef9ea | ||
|
|
f3fcc28f89 | ||
|
|
7cfdf46724 | ||
|
|
d681451297 | ||
|
|
5987a6d060 | ||
|
|
e2ccefb2f4 | ||
|
|
103b31a569 | ||
|
|
004e058353 | ||
|
|
5157f0bbb2 | ||
|
|
8bbcf31b83 | ||
|
|
9e814315dd | ||
|
|
0ea0256623 | ||
|
|
fb8868c854 | ||
|
|
ea4964052d | ||
|
|
268e2f114f |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -27,8 +27,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Extract version from commit message
|
- name: Extract version from commit message
|
||||||
id: extract
|
id: extract
|
||||||
|
env:
|
||||||
|
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||||
run: |
|
run: |
|
||||||
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
||||||
# Only tag versions on main branch
|
# Only tag versions on main branch
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
|
if [ "${{ github.ref }}" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
|
||||||
VERSION="${BASH_REMATCH[1]}"
|
VERSION="${BASH_REMATCH[1]}"
|
||||||
|
|||||||
@@ -124,11 +124,44 @@ Choose between four types of loops:
|
|||||||
3. Drag other blocks inside the loop container
|
3. Drag other blocks inside the loop container
|
||||||
4. Connect the blocks as needed
|
4. Connect the blocks as needed
|
||||||
|
|
||||||
### Accessing Results
|
### Referencing Loop Data
|
||||||
|
|
||||||
After a loop completes, you can access aggregated results:
|
There's an important distinction between referencing loop data from **inside** vs **outside** the loop:
|
||||||
|
|
||||||
- **`<loop.results>`**: Array of results from all loop iterations
|
<Tabs items={['Inside the Loop', 'Outside the Loop']}>
|
||||||
|
<Tab>
|
||||||
|
**Inside the loop**, use `<loop.>` references to access the current iteration context:
|
||||||
|
|
||||||
|
- **`<loop.index>`**: Current iteration number (0-based)
|
||||||
|
- **`<loop.currentItem>`**: Current item being processed (forEach only)
|
||||||
|
- **`<loop.items>`**: Full collection being iterated (forEach only)
|
||||||
|
|
||||||
|
```
|
||||||
|
// Inside a Function block within the loop
|
||||||
|
const idx = <loop.index>; // 0, 1, 2, ...
|
||||||
|
const item = <loop.currentItem>; // Current item
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
These references are only available for blocks **inside** the loop container. They give you access to the current iteration's context.
|
||||||
|
</Callout>
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
**Outside the loop** (after it completes), reference the loop block by its name to access aggregated results:
|
||||||
|
|
||||||
|
- **`<LoopBlockName.results>`**: Array of results from all iterations
|
||||||
|
|
||||||
|
```
|
||||||
|
// If your loop block is named "Process Items"
|
||||||
|
const allResults = <processitems.results>;
|
||||||
|
// Returns: [result1, result2, result3, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
After the loop completes, use the loop's block name (not `loop.`) to access the collected results. The block name is normalized (lowercase, no spaces).
|
||||||
|
</Callout>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## Example Use Cases
|
## Example Use Cases
|
||||||
|
|
||||||
@@ -184,28 +217,29 @@ Variables (i=0) → Loop (While i<10) → Agent (Process) → Variables (i++)
|
|||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
|
Available **inside** the loop only:
|
||||||
<ul className="list-disc space-y-2 pl-6">
|
<ul className="list-disc space-y-2 pl-6">
|
||||||
<li>
|
<li>
|
||||||
<strong>loop.currentItem</strong>: Current item being processed
|
<strong>{"<loop.index>"}</strong>: Current iteration number (0-based)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>loop.index</strong>: Current iteration number (0-based)
|
<strong>{"<loop.currentItem>"}</strong>: Current item being processed (forEach only)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>loop.items</strong>: Full collection (forEach loops)
|
<strong>{"<loop.items>"}</strong>: Full collection (forEach only)
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
<ul className="list-disc space-y-2 pl-6">
|
<ul className="list-disc space-y-2 pl-6">
|
||||||
<li>
|
<li>
|
||||||
<strong>loop.results</strong>: Array of all iteration results
|
<strong>{"<blockname.results>"}</strong>: Array of all iteration results (accessed via block name)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Structure</strong>: Results maintain iteration order
|
<strong>Structure</strong>: Results maintain iteration order
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Access</strong>: Available in blocks after the loop
|
<strong>Access</strong>: Available in blocks after the loop completes
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -76,11 +76,44 @@ Choose between two types of parallel execution:
|
|||||||
3. Drag a single block inside the parallel container
|
3. Drag a single block inside the parallel container
|
||||||
4. Connect the block as needed
|
4. Connect the block as needed
|
||||||
|
|
||||||
### Accessing Results
|
### Referencing Parallel Data
|
||||||
|
|
||||||
After a parallel block completes, you can access aggregated results:
|
There's an important distinction between referencing parallel data from **inside** vs **outside** the parallel block:
|
||||||
|
|
||||||
- **`<parallel.results>`**: Array of results from all parallel instances
|
<Tabs items={['Inside the Parallel', 'Outside the Parallel']}>
|
||||||
|
<Tab>
|
||||||
|
**Inside the parallel**, use `<parallel.>` references to access the current instance context:
|
||||||
|
|
||||||
|
- **`<parallel.index>`**: Current instance number (0-based)
|
||||||
|
- **`<parallel.currentItem>`**: Item for this instance (collection-based only)
|
||||||
|
- **`<parallel.items>`**: Full collection being distributed (collection-based only)
|
||||||
|
|
||||||
|
```
|
||||||
|
// Inside a Function block within the parallel
|
||||||
|
const idx = <parallel.index>; // 0, 1, 2, ...
|
||||||
|
const item = <parallel.currentItem>; // This instance's item
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
These references are only available for blocks **inside** the parallel container. They give you access to the current instance's context.
|
||||||
|
</Callout>
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
**Outside the parallel** (after it completes), reference the parallel block by its name to access aggregated results:
|
||||||
|
|
||||||
|
- **`<ParallelBlockName.results>`**: Array of results from all instances
|
||||||
|
|
||||||
|
```
|
||||||
|
// If your parallel block is named "Process Tasks"
|
||||||
|
const allResults = <processtasks.results>;
|
||||||
|
// Returns: [result1, result2, result3, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
After the parallel completes, use the parallel's block name (not `parallel.`) to access the collected results. The block name is normalized (lowercase, no spaces).
|
||||||
|
</Callout>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## Example Use Cases
|
## Example Use Cases
|
||||||
|
|
||||||
@@ -98,11 +131,11 @@ Parallel (["gpt-4o", "claude-3.7-sonnet", "gemini-2.5-pro"]) → Agent → Evalu
|
|||||||
|
|
||||||
### Result Aggregation
|
### Result Aggregation
|
||||||
|
|
||||||
Results from all parallel instances are automatically collected:
|
Results from all parallel instances are automatically collected and accessible via the block name:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// In a Function block after the parallel
|
// In a Function block after a parallel named "Process Tasks"
|
||||||
const allResults = input.parallel.results;
|
const allResults = <processtasks.results>;
|
||||||
// Returns: [result1, result2, result3, ...]
|
// Returns: [result1, result2, result3, ...]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -158,25 +191,26 @@ Understanding when to use each:
|
|||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
|
Available **inside** the parallel only:
|
||||||
<ul className="list-disc space-y-2 pl-6">
|
<ul className="list-disc space-y-2 pl-6">
|
||||||
<li>
|
<li>
|
||||||
<strong>parallel.currentItem</strong>: Item for this instance
|
<strong>{"<parallel.index>"}</strong>: Instance number (0-based)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>parallel.index</strong>: Instance number (0-based)
|
<strong>{"<parallel.currentItem>"}</strong>: Item for this instance (collection-based only)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>parallel.items</strong>: Full collection (collection-based)
|
<strong>{"<parallel.items>"}</strong>: Full collection (collection-based only)
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
<ul className="list-disc space-y-2 pl-6">
|
<ul className="list-disc space-y-2 pl-6">
|
||||||
<li>
|
<li>
|
||||||
<strong>parallel.results</strong>: Array of all instance results
|
<strong>{"<blockname.results>"}</strong>: Array of all instance results (accessed via block name)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Access</strong>: Available in blocks after the parallel
|
<strong>Access</strong>: Available in blocks after the parallel completes
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
|
import { Eye, EyeOff } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -22,8 +21,10 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
|
|||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
|
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||||
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'
|
||||||
|
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||||
|
|
||||||
const logger = createLogger('LoginForm')
|
const logger = createLogger('LoginForm')
|
||||||
|
|
||||||
@@ -105,8 +106,7 @@ export default function LoginPage({
|
|||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||||
const [showValidationError, setShowValidationError] = useState(false)
|
const [showValidationError, setShowValidationError] = useState(false)
|
||||||
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
const buttonClass = useBrandedButtonClass()
|
||||||
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
|
||||||
|
|
||||||
const [callbackUrl, setCallbackUrl] = useState('/workspace')
|
const [callbackUrl, setCallbackUrl] = useState('/workspace')
|
||||||
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
||||||
@@ -114,7 +114,6 @@ export default function LoginPage({
|
|||||||
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
|
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
|
||||||
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('')
|
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('')
|
||||||
const [isSubmittingReset, setIsSubmittingReset] = useState(false)
|
const [isSubmittingReset, setIsSubmittingReset] = useState(false)
|
||||||
const [isResetButtonHovered, setIsResetButtonHovered] = useState(false)
|
|
||||||
const [resetStatus, setResetStatus] = useState<{
|
const [resetStatus, setResetStatus] = useState<{
|
||||||
type: 'success' | 'error' | null
|
type: 'success' | 'error' | null
|
||||||
message: string
|
message: string
|
||||||
@@ -123,6 +122,7 @@ export default function LoginPage({
|
|||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
||||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||||
|
const [resetSuccessMessage, setResetSuccessMessage] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
@@ -139,32 +139,12 @@ export default function LoginPage({
|
|||||||
|
|
||||||
const inviteFlow = searchParams.get('invite_flow') === 'true'
|
const inviteFlow = searchParams.get('invite_flow') === 'true'
|
||||||
setIsInviteFlow(inviteFlow)
|
setIsInviteFlow(inviteFlow)
|
||||||
}
|
|
||||||
|
|
||||||
const checkCustomBrand = () => {
|
const resetSuccess = searchParams.get('resetSuccess') === 'true'
|
||||||
const computedStyle = getComputedStyle(document.documentElement)
|
if (resetSuccess) {
|
||||||
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
setResetSuccessMessage('Password reset successful. Please sign in with your new password.')
|
||||||
|
|
||||||
if (brandAccent && brandAccent !== '#6f3dfa') {
|
|
||||||
setButtonClass('branded-button-custom')
|
|
||||||
} else {
|
|
||||||
setButtonClass('branded-button-gradient')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCustomBrand()
|
|
||||||
|
|
||||||
window.addEventListener('resize', checkCustomBrand)
|
|
||||||
const observer = new MutationObserver(checkCustomBrand)
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['style', 'class'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', checkCustomBrand)
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -202,6 +182,13 @@ export default function LoginPage({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const redirectToVerify = (emailToVerify: string) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
sessionStorage.setItem('verificationEmail', emailToVerify)
|
||||||
|
}
|
||||||
|
router.push('/verify')
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData(e.currentTarget)
|
const formData = new FormData(e.currentTarget)
|
||||||
const emailRaw = formData.get('email') as string
|
const emailRaw = formData.get('email') as string
|
||||||
const email = emailRaw.trim().toLowerCase()
|
const email = emailRaw.trim().toLowerCase()
|
||||||
@@ -221,6 +208,7 @@ export default function LoginPage({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
|
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
|
||||||
|
let errorHandled = false
|
||||||
|
|
||||||
const result = await client.signIn.email(
|
const result = await client.signIn.email(
|
||||||
{
|
{
|
||||||
@@ -231,11 +219,16 @@ export default function LoginPage({
|
|||||||
{
|
{
|
||||||
onError: (ctx) => {
|
onError: (ctx) => {
|
||||||
logger.error('Login error:', ctx.error)
|
logger.error('Login error:', ctx.error)
|
||||||
const errorMessage: string[] = ['Invalid email or password']
|
|
||||||
|
|
||||||
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
|
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
|
||||||
|
errorHandled = true
|
||||||
|
redirectToVerify(email)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorHandled = true
|
||||||
|
const errorMessage: string[] = ['Invalid email or password']
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ctx.error.code?.includes('BAD_REQUEST') ||
|
ctx.error.code?.includes('BAD_REQUEST') ||
|
||||||
ctx.error.message?.includes('Email and password sign in is not enabled')
|
ctx.error.message?.includes('Email and password sign in is not enabled')
|
||||||
@@ -271,6 +264,7 @@ export default function LoginPage({
|
|||||||
errorMessage.push('Too many requests. Please wait a moment before trying again.')
|
errorMessage.push('Too many requests. Please wait a moment before trying again.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setResetSuccessMessage(null)
|
||||||
setPasswordErrors(errorMessage)
|
setPasswordErrors(errorMessage)
|
||||||
setShowValidationError(true)
|
setShowValidationError(true)
|
||||||
},
|
},
|
||||||
@@ -278,15 +272,25 @@ export default function LoginPage({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!result || result.error) {
|
if (!result || result.error) {
|
||||||
|
// Show error if not already handled by onError callback
|
||||||
|
if (!errorHandled) {
|
||||||
|
setResetSuccessMessage(null)
|
||||||
|
const errorMessage = result?.error?.message || 'Login failed. Please try again.'
|
||||||
|
setPasswordErrors([errorMessage])
|
||||||
|
setShowValidationError(true)
|
||||||
|
}
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear reset success message on successful login
|
||||||
|
setResetSuccessMessage(null)
|
||||||
|
|
||||||
|
// Explicit redirect fallback if better-auth doesn't redirect
|
||||||
|
router.push(safeCallbackUrl)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) {
|
if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) {
|
||||||
if (typeof window !== 'undefined') {
|
redirectToVerify(email)
|
||||||
sessionStorage.setItem('verificationEmail', email)
|
|
||||||
}
|
|
||||||
router.push('/verify')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,6 +404,13 @@ export default function LoginPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Password reset success message */}
|
||||||
|
{resetSuccessMessage && (
|
||||||
|
<div className={`${inter.className} mt-1 space-y-1 text-[#4CAF50] text-xs`}>
|
||||||
|
<p>{resetSuccessMessage}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Email/Password Form - show unless explicitly disabled */}
|
{/* Email/Password Form - show unless explicitly disabled */}
|
||||||
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
|
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
|
||||||
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
|
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
|
||||||
@@ -482,24 +493,14 @@ export default function LoginPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<BrandedButton
|
||||||
type='submit'
|
type='submit'
|
||||||
onMouseEnter={() => setIsButtonHovered(true)}
|
|
||||||
onMouseLeave={() => setIsButtonHovered(false)}
|
|
||||||
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
loading={isLoading}
|
||||||
|
loadingText='Signing in'
|
||||||
>
|
>
|
||||||
<span className='flex items-center gap-1'>
|
Sign in
|
||||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
</BrandedButton>
|
||||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
|
||||||
{isButtonHovered ? (
|
|
||||||
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -610,25 +611,15 @@ export default function LoginPage({
|
|||||||
<p>{resetStatus.message}</p>
|
<p>{resetStatus.message}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
<BrandedButton
|
||||||
type='button'
|
type='button'
|
||||||
onClick={handleForgotPassword}
|
onClick={handleForgotPassword}
|
||||||
onMouseEnter={() => setIsResetButtonHovered(true)}
|
|
||||||
onMouseLeave={() => setIsResetButtonHovered(false)}
|
|
||||||
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
|
||||||
disabled={isSubmittingReset}
|
disabled={isSubmittingReset}
|
||||||
|
loading={isSubmittingReset}
|
||||||
|
loadingText='Sending'
|
||||||
>
|
>
|
||||||
<span className='flex items-center gap-1'>
|
Send Reset Link
|
||||||
{isSubmittingReset ? 'Sending...' : 'Send Reset Link'}
|
</BrandedButton>
|
||||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
|
||||||
{isResetButtonHovered ? (
|
|
||||||
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
|
import { Eye, EyeOff } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
|
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||||
|
|
||||||
interface RequestResetFormProps {
|
interface RequestResetFormProps {
|
||||||
email: string
|
email: string
|
||||||
@@ -27,36 +27,6 @@ export function RequestResetForm({
|
|||||||
statusMessage,
|
statusMessage,
|
||||||
className,
|
className,
|
||||||
}: RequestResetFormProps) {
|
}: RequestResetFormProps) {
|
||||||
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
|
||||||
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkCustomBrand = () => {
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement)
|
|
||||||
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
|
||||||
|
|
||||||
if (brandAccent && brandAccent !== '#6f3dfa') {
|
|
||||||
setButtonClass('branded-button-custom')
|
|
||||||
} else {
|
|
||||||
setButtonClass('branded-button-gradient')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCustomBrand()
|
|
||||||
|
|
||||||
window.addEventListener('resize', checkCustomBrand)
|
|
||||||
const observer = new MutationObserver(checkCustomBrand)
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['style', 'class'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', checkCustomBrand)
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onSubmit(email)
|
onSubmit(email)
|
||||||
@@ -94,24 +64,14 @@ export function RequestResetForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<BrandedButton
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onMouseEnter={() => setIsButtonHovered(true)}
|
loading={isSubmitting}
|
||||||
onMouseLeave={() => setIsButtonHovered(false)}
|
loadingText='Sending'
|
||||||
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
|
||||||
>
|
>
|
||||||
<span className='flex items-center gap-1'>
|
Send Reset Link
|
||||||
{isSubmitting ? 'Sending...' : 'Send Reset Link'}
|
</BrandedButton>
|
||||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
|
||||||
{isButtonHovered ? (
|
|
||||||
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -138,35 +98,6 @@ export function SetNewPasswordForm({
|
|||||||
const [validationMessage, setValidationMessage] = useState('')
|
const [validationMessage, setValidationMessage] = useState('')
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||||
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
|
||||||
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkCustomBrand = () => {
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement)
|
|
||||||
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
|
||||||
|
|
||||||
if (brandAccent && brandAccent !== '#6f3dfa') {
|
|
||||||
setButtonClass('branded-button-custom')
|
|
||||||
} else {
|
|
||||||
setButtonClass('branded-button-gradient')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCustomBrand()
|
|
||||||
|
|
||||||
window.addEventListener('resize', checkCustomBrand)
|
|
||||||
const observer = new MutationObserver(checkCustomBrand)
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['style', 'class'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', checkCustomBrand)
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -296,24 +227,14 @@ export function SetNewPasswordForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<BrandedButton
|
||||||
disabled={isSubmitting || !token}
|
|
||||||
type='submit'
|
type='submit'
|
||||||
onMouseEnter={() => setIsButtonHovered(true)}
|
disabled={isSubmitting || !token}
|
||||||
onMouseLeave={() => setIsButtonHovered(false)}
|
loading={isSubmitting}
|
||||||
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
loadingText='Resetting'
|
||||||
>
|
>
|
||||||
<span className='flex items-center gap-1'>
|
Reset Password
|
||||||
{isSubmitting ? 'Resetting...' : 'Reset Password'}
|
</BrandedButton>
|
||||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
|
||||||
{isButtonHovered ? (
|
|
||||||
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
import { Suspense, useEffect, useState } from 'react'
|
import { Suspense, useEffect, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
|
import { Eye, EyeOff } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { client, useSession } from '@/lib/auth/auth-client'
|
import { client, useSession } from '@/lib/auth/auth-client'
|
||||||
@@ -14,8 +13,10 @@ import { cn } from '@/lib/core/utils/cn'
|
|||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
|
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||||
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'
|
||||||
|
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||||
|
|
||||||
const logger = createLogger('SignupForm')
|
const logger = createLogger('SignupForm')
|
||||||
|
|
||||||
@@ -95,8 +96,7 @@ function SignupFormContent({
|
|||||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||||
const [redirectUrl, setRedirectUrl] = useState('')
|
const [redirectUrl, setRedirectUrl] = useState('')
|
||||||
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
||||||
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
const buttonClass = useBrandedButtonClass()
|
||||||
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
|
||||||
|
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [nameErrors, setNameErrors] = useState<string[]>([])
|
const [nameErrors, setNameErrors] = useState<string[]>([])
|
||||||
@@ -126,31 +126,6 @@ function SignupFormContent({
|
|||||||
if (inviteFlowParam === 'true') {
|
if (inviteFlowParam === 'true') {
|
||||||
setIsInviteFlow(true)
|
setIsInviteFlow(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkCustomBrand = () => {
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement)
|
|
||||||
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
|
||||||
|
|
||||||
if (brandAccent && brandAccent !== '#6f3dfa') {
|
|
||||||
setButtonClass('branded-button-custom')
|
|
||||||
} else {
|
|
||||||
setButtonClass('branded-button-gradient')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCustomBrand()
|
|
||||||
|
|
||||||
window.addEventListener('resize', checkCustomBrand)
|
|
||||||
const observer = new MutationObserver(checkCustomBrand)
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['style', 'class'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', checkCustomBrand)
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
const validatePassword = (passwordValue: string): string[] => {
|
const validatePassword = (passwordValue: string): string[] => {
|
||||||
@@ -500,24 +475,14 @@ function SignupFormContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<BrandedButton
|
||||||
type='submit'
|
type='submit'
|
||||||
onMouseEnter={() => setIsButtonHovered(true)}
|
|
||||||
onMouseLeave={() => setIsButtonHovered(false)}
|
|
||||||
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
loading={isLoading}
|
||||||
|
loadingText='Creating account'
|
||||||
>
|
>
|
||||||
<span className='flex items-center gap-1'>
|
Create account
|
||||||
{isLoading ? 'Creating account' : 'Create account'}
|
</BrandedButton>
|
||||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
|
||||||
{isButtonHovered ? (
|
|
||||||
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { cn } from '@/lib/core/utils/cn'
|
|||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
|
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||||
|
|
||||||
const logger = createLogger('SSOForm')
|
const logger = createLogger('SSOForm')
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export default function SSOForm() {
|
|||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
||||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||||
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
const buttonClass = useBrandedButtonClass()
|
||||||
const [callbackUrl, setCallbackUrl] = useState('/workspace')
|
const [callbackUrl, setCallbackUrl] = useState('/workspace')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -90,31 +91,6 @@ export default function SSOForm() {
|
|||||||
setShowEmailValidationError(true)
|
setShowEmailValidationError(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkCustomBrand = () => {
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement)
|
|
||||||
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
|
||||||
|
|
||||||
if (brandAccent && brandAccent !== '#6f3dfa') {
|
|
||||||
setButtonClass('branded-button-custom')
|
|
||||||
} else {
|
|
||||||
setButtonClass('branded-button-gradient')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCustomBrand()
|
|
||||||
|
|
||||||
window.addEventListener('resize', checkCustomBrand)
|
|
||||||
const observer = new MutationObserver(checkCustomBrand)
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['style', 'class'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', checkCustomBrand)
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { cn } from '@/lib/core/utils/cn'
|
|||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
import { useVerification } from '@/app/(auth)/verify/use-verification'
|
import { useVerification } from '@/app/(auth)/verify/use-verification'
|
||||||
|
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||||
|
|
||||||
interface VerifyContentProps {
|
interface VerifyContentProps {
|
||||||
hasEmailService: boolean
|
hasEmailService: boolean
|
||||||
@@ -58,34 +59,7 @@ function VerificationForm({
|
|||||||
setCountdown(30)
|
setCountdown(30)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
const buttonClass = useBrandedButtonClass()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkCustomBrand = () => {
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement)
|
|
||||||
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
|
||||||
|
|
||||||
if (brandAccent && brandAccent !== '#6f3dfa') {
|
|
||||||
setButtonClass('branded-button-custom')
|
|
||||||
} else {
|
|
||||||
setButtonClass('branded-button-gradient')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCustomBrand()
|
|
||||||
|
|
||||||
window.addEventListener('resize', checkCustomBrand)
|
|
||||||
const observer = new MutationObserver(checkCustomBrand)
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['style', 'class'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', checkCustomBrand)
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useRef, useState } from 'react'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { X } from 'lucide-react'
|
import { X } from 'lucide-react'
|
||||||
import { Textarea } from '@/components/emcn'
|
import { Textarea } from '@/components/emcn'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +17,7 @@ import { isHosted } from '@/lib/core/config/feature-flags'
|
|||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
|
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||||
import Footer from '@/app/(landing)/components/footer/footer'
|
import Footer from '@/app/(landing)/components/footer/footer'
|
||||||
import Nav from '@/app/(landing)/components/nav/nav'
|
import Nav from '@/app/(landing)/components/nav/nav'
|
||||||
|
|
||||||
@@ -493,18 +493,17 @@ export default function CareersPage() {
|
|||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className='flex justify-end pt-2'>
|
<div className='flex justify-end pt-2'>
|
||||||
<Button
|
<BrandedButton
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={isSubmitting || submitStatus === 'success'}
|
disabled={isSubmitting || submitStatus === 'success'}
|
||||||
className='min-w-[200px] rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all duration-300 hover:opacity-90 disabled:opacity-50'
|
loading={isSubmitting}
|
||||||
size='lg'
|
loadingText='Submitting'
|
||||||
|
showArrow={false}
|
||||||
|
fullWidth={false}
|
||||||
|
className='min-w-[200px]'
|
||||||
>
|
>
|
||||||
{isSubmitting
|
{submitStatus === 'success' ? 'Submitted' : 'Submit Application'}
|
||||||
? 'Submitting...'
|
</BrandedButton>
|
||||||
: submitStatus === 'success'
|
|
||||||
? 'Submitted'
|
|
||||||
: 'Submit Application'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function StatusIndicator() {
|
|||||||
href={statusUrl}
|
href={statusUrl}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className={`flex items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
|
className={`flex min-w-[165px] items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
|
||||||
aria-label={`System status: ${message}`}
|
aria-label={`System status: ${message}`}
|
||||||
>
|
>
|
||||||
<StatusDotIcon status={status} className='h-[6px] w-[6px]' aria-hidden='true' />
|
<StatusDotIcon status={status} className='h-[6px] w-[6px]' aria-hidden='true' />
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export { LandingLoopNode } from './landing-canvas/landing-block/landing-loop-nod
|
|||||||
export { LandingNode } from './landing-canvas/landing-block/landing-node'
|
export { LandingNode } from './landing-canvas/landing-block/landing-node'
|
||||||
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
||||||
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
||||||
export type { TagProps } from './landing-canvas/landing-block/tag'
|
export type { SubBlockRowProps, TagProps } from './landing-canvas/landing-block/tag'
|
||||||
export { Tag } from './landing-canvas/landing-block/tag'
|
export { SubBlockRow, Tag } from './landing-canvas/landing-block/tag'
|
||||||
export type {
|
export type {
|
||||||
LandingBlockNode,
|
LandingBlockNode,
|
||||||
LandingCanvasProps,
|
LandingCanvasProps,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { BookIcon } from 'lucide-react'
|
|
||||||
import {
|
import {
|
||||||
Tag,
|
SubBlockRow,
|
||||||
type TagProps,
|
type SubBlockRowProps,
|
||||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data structure for a landing card component
|
* Data structure for a landing card component
|
||||||
|
* Matches the workflow block structure from the application
|
||||||
*/
|
*/
|
||||||
export interface LandingCardData {
|
export interface LandingCardData {
|
||||||
/** Icon element to display in the card header */
|
/** Icon element to display in the card header */
|
||||||
@@ -15,8 +15,8 @@ export interface LandingCardData {
|
|||||||
color: string | '#f6f6f6'
|
color: string | '#f6f6f6'
|
||||||
/** Name/title of the card */
|
/** Name/title of the card */
|
||||||
name: string
|
name: string
|
||||||
/** Optional tags to display at the bottom of the card */
|
/** Optional subblock rows to display below the header */
|
||||||
tags?: TagProps[]
|
tags?: SubBlockRowProps[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +28,8 @@ export interface LandingBlockProps extends LandingCardData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing block component that displays a card with icon, name, and optional tags
|
* Landing block component that displays a card with icon, name, and optional subblock rows
|
||||||
|
* Styled to match the application's workflow blocks
|
||||||
* @param props - Component properties including icon, color, name, tags, and className
|
* @param props - Component properties including icon, color, name, tags, and className
|
||||||
* @returns A styled block card component
|
* @returns A styled block card component
|
||||||
*/
|
*/
|
||||||
@@ -39,33 +40,37 @@ export const LandingBlock = React.memo(function LandingBlock({
|
|||||||
tags,
|
tags,
|
||||||
className,
|
className,
|
||||||
}: LandingBlockProps) {
|
}: LandingBlockProps) {
|
||||||
|
const hasContentBelowHeader = tags && tags.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`z-10 flex w-64 flex-col items-start gap-3 rounded-[14px] border border-[#E5E5E5] bg-[#FEFEFE] p-3 ${className ?? ''}`}
|
className={`z-10 flex w-[250px] flex-col rounded-[8px] border border-[#E5E5E5] bg-white ${className ?? ''}`}
|
||||||
style={{
|
|
||||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className='flex w-full items-center justify-between'>
|
{/* Header - matches workflow-block.tsx header styling */}
|
||||||
<div className='flex items-center gap-2.5'>
|
<div
|
||||||
|
className={`flex items-center justify-between p-[8px] ${hasContentBelowHeader ? 'border-[#E5E5E5] border-b' : ''}`}
|
||||||
|
>
|
||||||
|
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
|
||||||
<div
|
<div
|
||||||
className='flex h-6 w-6 items-center justify-center rounded-[8px] text-white'
|
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
|
||||||
style={{ backgroundColor: color as string }}
|
style={{ background: color as string }}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-base text-card-foreground'>{name}</p>
|
<span className='truncate font-medium text-[#171717] text-[16px]' title={name}>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<BookIcon className='h-4 w-4 text-muted-foreground' />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tags && tags.length > 0 ? (
|
{/* Content - SubBlock Rows matching workflow-block.tsx */}
|
||||||
<div className='flex flex-wrap gap-2'>
|
{hasContentBelowHeader && (
|
||||||
|
<div className='flex flex-col gap-[8px] p-[8px]'>
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<Tag key={tag.label} icon={tag.icon} label={tag.label} />
|
<SubBlockRow key={tag.label} icon={tag.icon} label={tag.label} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ import {
|
|||||||
type LandingCardData,
|
type LandingCardData,
|
||||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Y offset from block top - matches HANDLE_POSITIONS.DEFAULT_Y_OFFSET
|
||||||
|
*/
|
||||||
|
const HANDLE_Y_OFFSET = 20
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React Flow node component for the landing canvas
|
* React Flow node component for the landing canvas
|
||||||
* Includes CSS animations and connection handles
|
* Styled to match the application's workflow blocks
|
||||||
* @param props - Component properties containing node data
|
* @param props - Component properties containing node data
|
||||||
* @returns A React Flow compatible node component
|
* @returns A React Flow compatible node component
|
||||||
*/
|
*/
|
||||||
@@ -41,15 +46,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
|||||||
type='target'
|
type='target'
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
style={{
|
style={{
|
||||||
width: '12px',
|
width: '7px',
|
||||||
height: '12px',
|
height: '20px',
|
||||||
background: '#FEFEFE',
|
background: '#D1D1D1',
|
||||||
border: '1px solid #E5E5E5',
|
border: 'none',
|
||||||
borderRadius: '50%',
|
borderRadius: '2px 0 0 2px',
|
||||||
top: '50%',
|
top: `${HANDLE_Y_OFFSET}px`,
|
||||||
left: '-20px',
|
left: '-7px',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 2,
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
/>
|
/>
|
||||||
@@ -59,15 +64,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
|||||||
type='source'
|
type='source'
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
style={{
|
style={{
|
||||||
width: '12px',
|
width: '7px',
|
||||||
height: '12px',
|
height: '20px',
|
||||||
background: '#FEFEFE',
|
background: '#D1D1D1',
|
||||||
border: '1px solid #E5E5E5',
|
border: 'none',
|
||||||
borderRadius: '50%',
|
borderRadius: '0 2px 2px 0',
|
||||||
top: '50%',
|
top: `${HANDLE_Y_OFFSET}px`,
|
||||||
right: '-20px',
|
right: '-7px',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 2,
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface LoopBlockProps {
|
|||||||
/**
|
/**
|
||||||
* Loop block container component that provides a styled container
|
* Loop block container component that provides a styled container
|
||||||
* for grouping related elements with a dashed border
|
* for grouping related elements with a dashed border
|
||||||
|
* Styled to match the application's subflow containers
|
||||||
* @param props - Component properties including children and styling
|
* @param props - Component properties including children and styling
|
||||||
* @returns A styled loop container component
|
* @returns A styled loop container component
|
||||||
*/
|
*/
|
||||||
@@ -29,33 +30,33 @@ export const LoopBlock = React.memo(function LoopBlock({
|
|||||||
style={{
|
style={{
|
||||||
width: '1198px',
|
width: '1198px',
|
||||||
height: '528px',
|
height: '528px',
|
||||||
borderRadius: '14px',
|
borderRadius: '8px',
|
||||||
background: 'rgba(59, 130, 246, 0.10)',
|
background: 'rgba(59, 130, 246, 0.08)',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Custom dashed border with SVG */}
|
{/* Custom dashed border with SVG - 8px border radius to match blocks */}
|
||||||
<svg
|
<svg
|
||||||
className='pointer-events-none absolute inset-0 h-full w-full'
|
className='pointer-events-none absolute inset-0 h-full w-full'
|
||||||
style={{ borderRadius: '14px' }}
|
style={{ borderRadius: '8px' }}
|
||||||
preserveAspectRatio='none'
|
preserveAspectRatio='none'
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
className='landing-loop-animated-dash'
|
className='landing-loop-animated-dash'
|
||||||
d='M 1183.5 527.5
|
d='M 1190 527.5
|
||||||
L 14 527.5
|
L 8 527.5
|
||||||
A 13.5 13.5 0 0 1 0.5 514
|
A 7.5 7.5 0 0 1 0.5 520
|
||||||
L 0.5 14
|
L 0.5 8
|
||||||
A 13.5 13.5 0 0 1 14 0.5
|
A 7.5 7.5 0 0 1 8 0.5
|
||||||
L 1183.5 0.5
|
L 1190 0.5
|
||||||
A 13.5 13.5 0 0 1 1197 14
|
A 7.5 7.5 0 0 1 1197.5 8
|
||||||
L 1197 514
|
L 1197.5 520
|
||||||
A 13.5 13.5 0 0 1 1183.5 527.5 Z'
|
A 7.5 7.5 0 0 1 1190 527.5 Z'
|
||||||
fill='none'
|
fill='none'
|
||||||
stroke='#3B82F6'
|
stroke='#3B82F6'
|
||||||
strokeWidth='1'
|
strokeWidth='1'
|
||||||
strokeDasharray='12 12'
|
strokeDasharray='8 8'
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,25 +1,52 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties for a tag component
|
* Properties for a subblock row component
|
||||||
|
* Matches the SubBlockRow pattern from workflow-block.tsx
|
||||||
*/
|
*/
|
||||||
export interface TagProps {
|
export interface SubBlockRowProps {
|
||||||
/** Icon element to display in the tag */
|
/** Icon element to display (optional, for visual context) */
|
||||||
icon: React.ReactNode
|
icon?: React.ReactNode
|
||||||
/** Text label for the tag */
|
/** Text label for the row title */
|
||||||
label: string
|
label: string
|
||||||
|
/** Optional value to display on the right side */
|
||||||
|
value?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag component for displaying labeled icons in a compact format
|
* Kept for backwards compatibility
|
||||||
* @param props - Tag properties including icon and label
|
|
||||||
* @returns A styled tag component
|
|
||||||
*/
|
*/
|
||||||
export const Tag = React.memo(function Tag({ icon, label }: TagProps) {
|
export type TagProps = SubBlockRowProps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubBlockRow component matching the workflow block's subblock row style
|
||||||
|
* @param props - Row properties including label and optional value
|
||||||
|
* @returns A styled row component
|
||||||
|
*/
|
||||||
|
export const SubBlockRow = React.memo(function SubBlockRow({ label, value }: SubBlockRowProps) {
|
||||||
|
// Split label by colon to separate title and value if present
|
||||||
|
const [title, displayValue] = label.includes(':')
|
||||||
|
? label.split(':').map((s) => s.trim())
|
||||||
|
: [label, value]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex w-fit items-center gap-1 rounded-[8px] border border-gray-300 bg-white px-2 py-0.5'>
|
<div className='flex items-center gap-[8px]'>
|
||||||
<div className='h-3 w-3 text-muted-foreground'>{icon}</div>
|
<span className='min-w-0 truncate text-[#888888] text-[14px] capitalize' title={title}>
|
||||||
<p className='text-muted-foreground text-xs leading-normal'>{label}</p>
|
{title}
|
||||||
|
</span>
|
||||||
|
{displayValue && (
|
||||||
|
<span
|
||||||
|
className='flex-1 truncate text-right text-[#171717] text-[14px]'
|
||||||
|
title={displayValue}
|
||||||
|
>
|
||||||
|
{displayValue}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag component - alias for SubBlockRow for backwards compatibility
|
||||||
|
*/
|
||||||
|
export const Tag = SubBlockRow
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import { LandingFlow } from '@/app/(landing)/components/hero/components/landing-
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Visual constants for landing node dimensions
|
* Visual constants for landing node dimensions
|
||||||
|
* Matches BLOCK_DIMENSIONS from the application
|
||||||
*/
|
*/
|
||||||
export const CARD_WIDTH = 256
|
export const CARD_WIDTH = 250
|
||||||
export const CARD_HEIGHT = 92
|
export const CARD_HEIGHT = 100
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing block node with positioning information
|
* Landing block node with positioning information
|
||||||
|
|||||||
@@ -4,33 +4,29 @@ import React from 'react'
|
|||||||
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom edge component with animated dotted line that floats between handles
|
* Custom edge component with animated dashed line
|
||||||
|
* Styled to match the application's workflow edges with rectangular handles
|
||||||
* @param props - React Flow edge properties
|
* @param props - React Flow edge properties
|
||||||
* @returns An animated dotted edge component
|
* @returns An animated dashed edge component
|
||||||
*/
|
*/
|
||||||
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
||||||
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, data } =
|
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style } = props
|
||||||
props
|
|
||||||
|
|
||||||
// Adjust the connection points to create floating effect
|
// Adjust the connection points to connect flush with rectangular handles
|
||||||
// Account for handle size (12px) and additional spacing
|
// Handle width is 7px, positioned at -7px from edge
|
||||||
const handleRadius = 6 // Half of handle width (12px)
|
|
||||||
const floatingGap = 1 // Additional gap for floating effect
|
|
||||||
|
|
||||||
// Calculate adjusted positions based on edge direction
|
|
||||||
let adjustedSourceX = sourceX
|
let adjustedSourceX = sourceX
|
||||||
let adjustedTargetX = targetX
|
let adjustedTargetX = targetX
|
||||||
|
|
||||||
if (sourcePosition === Position.Right) {
|
if (sourcePosition === Position.Right) {
|
||||||
adjustedSourceX = sourceX + handleRadius + floatingGap
|
adjustedSourceX = sourceX + 1
|
||||||
} else if (sourcePosition === Position.Left) {
|
} else if (sourcePosition === Position.Left) {
|
||||||
adjustedSourceX = sourceX - handleRadius - floatingGap
|
adjustedSourceX = sourceX - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetPosition === Position.Left) {
|
if (targetPosition === Position.Left) {
|
||||||
adjustedTargetX = targetX - handleRadius - floatingGap
|
adjustedTargetX = targetX - 1
|
||||||
} else if (targetPosition === Position.Right) {
|
} else if (targetPosition === Position.Right) {
|
||||||
adjustedTargetX = targetX + handleRadius + floatingGap
|
adjustedTargetX = targetX + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const [path] = getSmoothStepPath({
|
const [path] = getSmoothStepPath({
|
||||||
@@ -40,8 +36,8 @@ export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
|||||||
targetY,
|
targetY,
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
borderRadius: 20,
|
borderRadius: 8,
|
||||||
offset: 10,
|
offset: 16,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import { ArrowUp, CodeIcon } from 'lucide-react'
|
||||||
ArrowUp,
|
|
||||||
BinaryIcon,
|
|
||||||
BookIcon,
|
|
||||||
CalendarIcon,
|
|
||||||
CodeIcon,
|
|
||||||
Globe2Icon,
|
|
||||||
MessageSquareIcon,
|
|
||||||
VariableIcon,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { type Edge, type Node, Position } from 'reactflow'
|
import { type Edge, type Node, Position } from 'reactflow'
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +14,6 @@ import {
|
|||||||
JiraIcon,
|
JiraIcon,
|
||||||
LinearIcon,
|
LinearIcon,
|
||||||
NotionIcon,
|
NotionIcon,
|
||||||
OpenAIIcon,
|
|
||||||
OutlookIcon,
|
OutlookIcon,
|
||||||
PackageSearchIcon,
|
PackageSearchIcon,
|
||||||
PineconeIcon,
|
PineconeIcon,
|
||||||
@@ -65,67 +55,56 @@ const SERVICE_TEMPLATES = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing blocks for the canvas preview
|
* Landing blocks for the canvas preview
|
||||||
|
* Styled to match the application's workflow blocks with subblock rows
|
||||||
*/
|
*/
|
||||||
const LANDING_BLOCKS: LandingManualBlock[] = [
|
const LANDING_BLOCKS: LandingManualBlock[] = [
|
||||||
{
|
{
|
||||||
id: 'schedule',
|
id: 'schedule',
|
||||||
name: 'Schedule',
|
name: 'Schedule',
|
||||||
color: '#7B68EE',
|
color: '#7B68EE',
|
||||||
icon: <ScheduleIcon className='h-4 w-4' />,
|
icon: <ScheduleIcon className='h-[16px] w-[16px] text-white' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 8, y: 60 },
|
mobile: { x: 8, y: 60 },
|
||||||
tablet: { x: 40, y: 120 },
|
tablet: { x: 40, y: 120 },
|
||||||
desktop: { x: 60, y: 180 },
|
desktop: { x: 60, y: 180 },
|
||||||
},
|
},
|
||||||
tags: [
|
tags: [{ label: 'Time: 09:00AM Daily' }, { label: 'Timezone: PST' }],
|
||||||
{ icon: <CalendarIcon className='h-3 w-3' />, label: '09:00AM Daily' },
|
|
||||||
{ icon: <Globe2Icon className='h-3 w-3' />, label: 'PST' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'knowledge',
|
id: 'knowledge',
|
||||||
name: 'Knowledge',
|
name: 'Knowledge',
|
||||||
color: '#00B0B0',
|
color: '#00B0B0',
|
||||||
icon: <PackageSearchIcon className='h-4 w-4' />,
|
icon: <PackageSearchIcon className='h-[16px] w-[16px] text-white' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 120, y: 140 },
|
mobile: { x: 120, y: 140 },
|
||||||
tablet: { x: 220, y: 200 },
|
tablet: { x: 220, y: 200 },
|
||||||
desktop: { x: 420, y: 241 },
|
desktop: { x: 420, y: 241 },
|
||||||
},
|
},
|
||||||
tags: [
|
tags: [{ label: 'Source: Product Vector DB' }, { label: 'Limit: 10' }],
|
||||||
{ icon: <BookIcon className='h-3 w-3' />, label: 'Product Vector DB' },
|
|
||||||
{ icon: <BinaryIcon className='h-3 w-3' />, label: 'Limit: 10' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'agent',
|
id: 'agent',
|
||||||
name: 'Agent',
|
name: 'Agent',
|
||||||
color: '#802FFF',
|
color: '#802FFF',
|
||||||
icon: <AgentIcon className='h-4 w-4' />,
|
icon: <AgentIcon className='h-[16px] w-[16px] text-white' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 340, y: 60 },
|
mobile: { x: 340, y: 60 },
|
||||||
tablet: { x: 540, y: 120 },
|
tablet: { x: 540, y: 120 },
|
||||||
desktop: { x: 880, y: 142 },
|
desktop: { x: 880, y: 142 },
|
||||||
},
|
},
|
||||||
tags: [
|
tags: [{ label: 'Model: gpt-5' }, { label: 'Prompt: You are a support ag...' }],
|
||||||
{ icon: <OpenAIIcon className='h-3 w-3' />, label: 'gpt-5' },
|
|
||||||
{ icon: <MessageSquareIcon className='h-3 w-3' />, label: 'You are a support ag...' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'function',
|
id: 'function',
|
||||||
name: 'Function',
|
name: 'Function',
|
||||||
color: '#FF402F',
|
color: '#FF402F',
|
||||||
icon: <CodeIcon className='h-4 w-4' />,
|
icon: <CodeIcon className='h-[16px] w-[16px] text-white' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 480, y: 220 },
|
mobile: { x: 480, y: 220 },
|
||||||
tablet: { x: 740, y: 280 },
|
tablet: { x: 740, y: 280 },
|
||||||
desktop: { x: 880, y: 340 },
|
desktop: { x: 880, y: 340 },
|
||||||
},
|
},
|
||||||
tags: [
|
tags: [{ label: 'Language: Python' }, { label: 'Code: time = "2025-09-01...' }],
|
||||||
{ icon: <CodeIcon className='h-3 w-3' />, label: 'Python' },
|
|
||||||
{ icon: <VariableIcon className='h-3 w-3' />, label: 'time = "2025-09-01...' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ function PricingCard({
|
|||||||
*/
|
*/
|
||||||
export default function LandingPricing() {
|
export default function LandingPricing() {
|
||||||
return (
|
return (
|
||||||
<section id='pricing' className='px-4 pt-[19px] sm:px-0 sm:pt-0' aria-label='Pricing plans'>
|
<section id='pricing' className='px-4 pt-[23px] sm:px-0 sm:pt-[4px]' aria-label='Pricing plans'>
|
||||||
<h2 className='sr-only'>Pricing Plans</h2>
|
<h2 className='sr-only'>Pricing Plans</h2>
|
||||||
<div className='relative mx-auto w-full max-w-[1289px]'>
|
<div className='relative mx-auto w-full max-w-[1289px]'>
|
||||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useBrandConfig } from '@/lib/branding/branding'
|
|||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
||||||
|
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||||
|
|
||||||
const logger = createLogger('nav')
|
const logger = createLogger('nav')
|
||||||
|
|
||||||
@@ -20,11 +21,12 @@ interface NavProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
||||||
const [githubStars, setGithubStars] = useState('25.1k')
|
const [githubStars, setGithubStars] = useState('26.1k')
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const brand = useBrandConfig()
|
const brand = useBrandConfig()
|
||||||
|
const buttonClass = useBrandedButtonClass()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (variant !== 'landing') return
|
if (variant !== 'landing') return
|
||||||
@@ -183,7 +185,7 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
|
|||||||
href='/signup'
|
href='/signup'
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
className='group inline-flex items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[14px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all sm:text-[16px]'
|
className={`${buttonClass} group inline-flex items-center justify-center gap-2 rounded-[10px] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white transition-all`}
|
||||||
aria-label='Get started with Sim - Sign up for free'
|
aria-label='Get started with Sim - Sign up for free'
|
||||||
prefetch={true}
|
prefetch={true}
|
||||||
>
|
>
|
||||||
|
|||||||
27
apps/sim/app/(landing)/studio/[slug]/back-link.tsx
Normal file
27
apps/sim/app/(landing)/studio/[slug]/back-link.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ArrowLeft, ChevronLeft } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export function BackLink() {
|
||||||
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href='/studio'
|
||||||
|
className='group flex items-center gap-1 text-gray-600 text-sm hover:text-gray-900'
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<span className='group-hover:-translate-x-0.5 inline-flex transition-transform duration-200'>
|
||||||
|
{isHovered ? (
|
||||||
|
<ArrowLeft className='h-4 w-4' aria-hidden='true' />
|
||||||
|
) : (
|
||||||
|
<ChevronLeft className='h-4 w-4' aria-hidden='true' />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
Back to Sim Studio
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,10 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
|
|||||||
import { FAQ } from '@/lib/blog/faq'
|
import { FAQ } from '@/lib/blog/faq'
|
||||||
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
||||||
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
||||||
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
|
import { BackLink } from '@/app/(landing)/studio/[slug]/back-link'
|
||||||
|
import { ShareButton } from '@/app/(landing)/studio/[slug]/share-button'
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const posts = await getAllPostMeta()
|
const posts = await getAllPostMeta()
|
||||||
@@ -48,9 +51,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
|||||||
/>
|
/>
|
||||||
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
|
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
|
||||||
<div className='mb-6'>
|
<div className='mb-6'>
|
||||||
<Link href='/studio' className='text-gray-600 text-sm hover:text-gray-900'>
|
<BackLink />
|
||||||
← Back to Sim Studio
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
|
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
|
||||||
<div className='w-full flex-shrink-0 md:w-[450px]'>
|
<div className='w-full flex-shrink-0 md:w-[450px]'>
|
||||||
@@ -75,28 +76,31 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
|||||||
>
|
>
|
||||||
{post.title}
|
{post.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className='mt-4 flex items-center gap-3'>
|
<div className='mt-4 flex items-center justify-between'>
|
||||||
{(post.authors || [post.author]).map((a, idx) => (
|
<div className='flex items-center gap-3'>
|
||||||
<div key={idx} className='flex items-center gap-2'>
|
{(post.authors || [post.author]).map((a, idx) => (
|
||||||
{a?.avatarUrl ? (
|
<div key={idx} className='flex items-center gap-2'>
|
||||||
<Avatar className='size-6'>
|
{a?.avatarUrl ? (
|
||||||
<AvatarImage src={a.avatarUrl} alt={a.name} />
|
<Avatar className='size-6'>
|
||||||
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
|
<AvatarImage src={a.avatarUrl} alt={a.name} />
|
||||||
</Avatar>
|
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
|
||||||
) : null}
|
</Avatar>
|
||||||
<Link
|
) : null}
|
||||||
href={a?.url || '#'}
|
<Link
|
||||||
target='_blank'
|
href={a?.url || '#'}
|
||||||
rel='noopener noreferrer author'
|
target='_blank'
|
||||||
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
|
rel='noopener noreferrer author'
|
||||||
itemProp='author'
|
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
|
||||||
itemScope
|
itemProp='author'
|
||||||
itemType='https://schema.org/Person'
|
itemScope
|
||||||
>
|
itemType='https://schema.org/Person'
|
||||||
<span itemProp='name'>{a?.name}</span>
|
>
|
||||||
</Link>
|
<span itemProp='name'>{a?.name}</span>
|
||||||
</div>
|
</Link>
|
||||||
))}
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ShareButton url={`${getBaseUrl()}/studio/${slug}`} title={post.title} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
65
apps/sim/app/(landing)/studio/[slug]/share-button.tsx
Normal file
65
apps/sim/app/(landing)/studio/[slug]/share-button.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Share2 } from 'lucide-react'
|
||||||
|
import { Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
|
||||||
|
|
||||||
|
interface ShareButtonProps {
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareButton({ url, title }: ShareButtonProps) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
const handleCopyLink = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false)
|
||||||
|
setOpen(false)
|
||||||
|
}, 1000)
|
||||||
|
} catch {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShareTwitter = () => {
|
||||||
|
const tweetUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`
|
||||||
|
window.open(tweetUrl, '_blank', 'noopener,noreferrer')
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShareLinkedIn = () => {
|
||||||
|
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`
|
||||||
|
window.open(linkedInUrl, '_blank', 'noopener,noreferrer')
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
variant='secondary'
|
||||||
|
size='sm'
|
||||||
|
colorScheme='inverted'
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
className='flex items-center gap-1.5 text-gray-600 text-sm hover:text-gray-900'
|
||||||
|
aria-label='Share this post'
|
||||||
|
>
|
||||||
|
<Share2 className='h-4 w-4' />
|
||||||
|
<span>Share</span>
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align='end' minWidth={140}>
|
||||||
|
<PopoverItem onClick={handleCopyLink}>{copied ? 'Copied!' : 'Copy link'}</PopoverItem>
|
||||||
|
<PopoverItem onClick={handleShareTwitter}>Share on X</PopoverItem>
|
||||||
|
<PopoverItem onClick={handleShareLinkedIn}>Share on LinkedIn</PopoverItem>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ export default async function StudioIndex({
|
|||||||
? filtered.sort((a, b) => {
|
? filtered.sort((a, b) => {
|
||||||
if (a.featured && !b.featured) return -1
|
if (a.featured && !b.featured) return -1
|
||||||
if (!a.featured && b.featured) return 1
|
if (!a.featured && b.featured) return 1
|
||||||
return 0
|
return new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||||
})
|
})
|
||||||
: filtered
|
: filtered
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
|||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { getRedisClient } from '@/lib/core/config/redis'
|
import { getRedisClient } from '@/lib/core/config/redis'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
const logger = createLogger('A2AAgentCardAPI')
|
const logger = createLogger('A2AAgentCardAPI')
|
||||||
|
|
||||||
@@ -95,6 +96,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<Ro
|
|||||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
|
||||||
|
if (!workspaceAccess.canWrite) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -160,6 +166,11 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
|
|||||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
|
||||||
|
if (!workspaceAccess.canWrite) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))
|
await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))
|
||||||
|
|
||||||
logger.info(`Deleted A2A agent: ${agentId}`)
|
logger.info(`Deleted A2A agent: ${agentId}`)
|
||||||
@@ -194,6 +205,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
|||||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
|
||||||
|
if (!workspaceAccess.canWrite) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const action = body.action as 'publish' | 'unpublish' | 'refresh'
|
const action = body.action as 'publish' | 'unpublish' | 'refresh'
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { getBrandConfig } from '@/lib/branding/branding'
|
import { getBrandConfig } from '@/lib/branding/branding'
|
||||||
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
|
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
|
||||||
|
import { validateExternalUrl } from '@/lib/core/security/input-validation'
|
||||||
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
||||||
@@ -1118,17 +1119,13 @@ async function handlePushNotificationSet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const urlValidation = validateExternalUrl(
|
||||||
const url = new URL(params.pushNotificationConfig.url)
|
params.pushNotificationConfig.url,
|
||||||
if (url.protocol !== 'https:') {
|
'Push notification URL'
|
||||||
return NextResponse.json(
|
)
|
||||||
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Push notification URL must use HTTPS'),
|
if (!urlValidation.isValid) {
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Invalid push notification URL'),
|
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, urlValidation.error || 'Invalid URL'),
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, desc, eq, inArray } from 'drizzle-orm'
|
import { and, desc, eq, inArray } from 'drizzle-orm'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { refreshOAuthToken } from '@/lib/oauth'
|
import { refreshOAuthToken } from '@/lib/oauth'
|
||||||
|
import {
|
||||||
|
getMicrosoftRefreshTokenExpiry,
|
||||||
|
isMicrosoftProvider,
|
||||||
|
PROACTIVE_REFRESH_THRESHOLD_DAYS,
|
||||||
|
} from '@/lib/oauth/microsoft'
|
||||||
|
|
||||||
const logger = createLogger('OAuthUtilsAPI')
|
const logger = createLogger('OAuthUtilsAPI')
|
||||||
|
|
||||||
@@ -205,15 +210,32 @@ export async function refreshAccessTokenIfNeeded(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decide if we should refresh: token missing OR expired
|
// Decide if we should refresh: token missing OR expired
|
||||||
const expiresAt = credential.accessTokenExpiresAt
|
const accessTokenExpiresAt = credential.accessTokenExpiresAt
|
||||||
|
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const shouldRefresh =
|
|
||||||
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
|
// Check if access token needs refresh (missing or expired)
|
||||||
|
const accessTokenNeedsRefresh =
|
||||||
|
!!credential.refreshToken &&
|
||||||
|
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
|
||||||
|
|
||||||
|
// Check if we should proactively refresh to prevent refresh token expiry
|
||||||
|
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
|
||||||
|
const proactiveRefreshThreshold = new Date(
|
||||||
|
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
|
||||||
|
)
|
||||||
|
const refreshTokenNeedsProactiveRefresh =
|
||||||
|
!!credential.refreshToken &&
|
||||||
|
isMicrosoftProvider(credential.providerId) &&
|
||||||
|
refreshTokenExpiresAt &&
|
||||||
|
refreshTokenExpiresAt <= proactiveRefreshThreshold
|
||||||
|
|
||||||
|
const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
|
||||||
|
|
||||||
const accessToken = credential.accessToken
|
const accessToken = credential.accessToken
|
||||||
|
|
||||||
if (shouldRefresh) {
|
if (shouldRefresh) {
|
||||||
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
|
logger.info(`[${requestId}] Refreshing token for credential`)
|
||||||
try {
|
try {
|
||||||
const refreshedToken = await refreshOAuthToken(
|
const refreshedToken = await refreshOAuthToken(
|
||||||
credential.providerId,
|
credential.providerId,
|
||||||
@@ -227,11 +249,15 @@ export async function refreshAccessTokenIfNeeded(
|
|||||||
userId: credential.userId,
|
userId: credential.userId,
|
||||||
hasRefreshToken: !!credential.refreshToken,
|
hasRefreshToken: !!credential.refreshToken,
|
||||||
})
|
})
|
||||||
|
if (!accessTokenNeedsRefresh && accessToken) {
|
||||||
|
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
|
||||||
|
return accessToken
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare update data
|
// Prepare update data
|
||||||
const updateData: any = {
|
const updateData: Record<string, unknown> = {
|
||||||
accessToken: refreshedToken.accessToken,
|
accessToken: refreshedToken.accessToken,
|
||||||
accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000),
|
accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -243,6 +269,10 @@ export async function refreshAccessTokenIfNeeded(
|
|||||||
updateData.refreshToken = refreshedToken.refreshToken
|
updateData.refreshToken = refreshedToken.refreshToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMicrosoftProvider(credential.providerId)) {
|
||||||
|
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
|
||||||
|
}
|
||||||
|
|
||||||
// Update the token in the database
|
// Update the token in the database
|
||||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||||
|
|
||||||
@@ -256,6 +286,10 @@ export async function refreshAccessTokenIfNeeded(
|
|||||||
credentialId,
|
credentialId,
|
||||||
userId: credential.userId,
|
userId: credential.userId,
|
||||||
})
|
})
|
||||||
|
if (!accessTokenNeedsRefresh && accessToken) {
|
||||||
|
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
|
||||||
|
return accessToken
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
} else if (!accessToken) {
|
} else if (!accessToken) {
|
||||||
@@ -277,10 +311,27 @@ export async function refreshTokenIfNeeded(
|
|||||||
credentialId: string
|
credentialId: string
|
||||||
): Promise<{ accessToken: string; refreshed: boolean }> {
|
): Promise<{ accessToken: string; refreshed: boolean }> {
|
||||||
// Decide if we should refresh: token missing OR expired
|
// Decide if we should refresh: token missing OR expired
|
||||||
const expiresAt = credential.accessTokenExpiresAt
|
const accessTokenExpiresAt = credential.accessTokenExpiresAt
|
||||||
|
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const shouldRefresh =
|
|
||||||
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
|
// Check if access token needs refresh (missing or expired)
|
||||||
|
const accessTokenNeedsRefresh =
|
||||||
|
!!credential.refreshToken &&
|
||||||
|
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
|
||||||
|
|
||||||
|
// Check if we should proactively refresh to prevent refresh token expiry
|
||||||
|
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
|
||||||
|
const proactiveRefreshThreshold = new Date(
|
||||||
|
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
|
||||||
|
)
|
||||||
|
const refreshTokenNeedsProactiveRefresh =
|
||||||
|
!!credential.refreshToken &&
|
||||||
|
isMicrosoftProvider(credential.providerId) &&
|
||||||
|
refreshTokenExpiresAt &&
|
||||||
|
refreshTokenExpiresAt <= proactiveRefreshThreshold
|
||||||
|
|
||||||
|
const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
|
||||||
|
|
||||||
// If token appears valid and present, return it directly
|
// If token appears valid and present, return it directly
|
||||||
if (!shouldRefresh) {
|
if (!shouldRefresh) {
|
||||||
@@ -293,13 +344,17 @@ export async function refreshTokenIfNeeded(
|
|||||||
|
|
||||||
if (!refreshResult) {
|
if (!refreshResult) {
|
||||||
logger.error(`[${requestId}] Failed to refresh token for credential`)
|
logger.error(`[${requestId}] Failed to refresh token for credential`)
|
||||||
|
if (!accessTokenNeedsRefresh && credential.accessToken) {
|
||||||
|
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
|
||||||
|
return { accessToken: credential.accessToken, refreshed: false }
|
||||||
|
}
|
||||||
throw new Error('Failed to refresh token')
|
throw new Error('Failed to refresh token')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult
|
const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult
|
||||||
|
|
||||||
// Prepare update data
|
// Prepare update data
|
||||||
const updateData: any = {
|
const updateData: Record<string, unknown> = {
|
||||||
accessToken: refreshedToken,
|
accessToken: refreshedToken,
|
||||||
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
|
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -311,6 +366,10 @@ export async function refreshTokenIfNeeded(
|
|||||||
updateData.refreshToken = newRefreshToken
|
updateData.refreshToken = newRefreshToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMicrosoftProvider(credential.providerId)) {
|
||||||
|
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
|
||||||
|
}
|
||||||
|
|
||||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||||
|
|
||||||
logger.info(`[${requestId}] Successfully refreshed access token`)
|
logger.info(`[${requestId}] Successfully refreshed access token`)
|
||||||
@@ -331,6 +390,11 @@ export async function refreshTokenIfNeeded(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!accessTokenNeedsRefresh && credential.accessToken) {
|
||||||
|
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
|
||||||
|
return { accessToken: credential.accessToken, refreshed: false }
|
||||||
|
}
|
||||||
|
|
||||||
logger.error(`[${requestId}] Refresh failed and no valid token found in DB`, error)
|
logger.error(`[${requestId}] Refresh failed and no valid token found in DB`, error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const resetPasswordSchema = z.object({
|
|||||||
.max(100, 'Password must not exceed 100 characters')
|
.max(100, 'Password must not exceed 100 characters')
|
||||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||||
.regex(/[0-9]/, 'Password must contain at least one number'),
|
.regex(/[0-9]/, 'Password must contain at least one number')
|
||||||
|
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
|
|||||||
@@ -104,17 +104,11 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Build execution params starting with LLM-provided arguments
|
// Build execution params starting with LLM-provided arguments
|
||||||
// Resolve all {{ENV_VAR}} references in the arguments
|
// Resolve all {{ENV_VAR}} references in the arguments (deep for nested objects)
|
||||||
const executionParams: Record<string, any> = resolveEnvVarReferences(
|
const executionParams: Record<string, any> = resolveEnvVarReferences(
|
||||||
toolArgs,
|
toolArgs,
|
||||||
decryptedEnvVars,
|
decryptedEnvVars,
|
||||||
{
|
{ deep: true }
|
||||||
resolveExactMatch: true,
|
|
||||||
allowEmbedded: true,
|
|
||||||
trimKeys: true,
|
|
||||||
onMissing: 'keep',
|
|
||||||
deep: true,
|
|
||||||
}
|
|
||||||
) as Record<string, any>
|
) as Record<string, any>
|
||||||
|
|
||||||
logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, {
|
logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, {
|
||||||
|
|||||||
@@ -84,6 +84,14 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
|
|||||||
|
|
||||||
vi.mock('@sim/logger', () => loggerMock)
|
vi.mock('@sim/logger', () => loggerMock)
|
||||||
|
|
||||||
|
vi.mock('@/lib/auth/hybrid', () => ({
|
||||||
|
checkInternalAuth: vi.fn().mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
userId: 'user-123',
|
||||||
|
authType: 'internal_jwt',
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/lib/execution/e2b', () => ({
|
vi.mock('@/lib/execution/e2b', () => ({
|
||||||
executeInE2B: vi.fn(),
|
executeInE2B: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@@ -110,6 +118,24 @@ describe('Function Execute API Route', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Security Tests', () => {
|
describe('Security Tests', () => {
|
||||||
|
it('should reject unauthorized requests', async () => {
|
||||||
|
const { checkInternalAuth } = await import('@/lib/auth/hybrid')
|
||||||
|
vi.mocked(checkInternalAuth).mockResolvedValueOnce({
|
||||||
|
success: false,
|
||||||
|
error: 'Unauthorized',
|
||||||
|
})
|
||||||
|
|
||||||
|
const req = createMockRequest('POST', {
|
||||||
|
code: 'return "test"',
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await POST(req)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
expect(response.status).toBe(401)
|
||||||
|
expect(data).toHaveProperty('error', 'Unauthorized')
|
||||||
|
})
|
||||||
|
|
||||||
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
|
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return "test"',
|
code: 'return "test"',
|
||||||
@@ -276,8 +302,11 @@ describe('Function Execute API Route', () => {
|
|||||||
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {
|
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <email>',
|
code: 'return <email>',
|
||||||
params: {
|
blockData: {
|
||||||
email: { id: '123', subject: 'Test Email' },
|
'block-123': { id: '123', subject: 'Test Email' },
|
||||||
|
},
|
||||||
|
blockNameMapping: {
|
||||||
|
email: 'block-123',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -305,9 +334,13 @@ describe('Function Execute API Route', () => {
|
|||||||
it.concurrent('should only match valid variable names in angle brackets', async () => {
|
it.concurrent('should only match valid variable names in angle brackets', async () => {
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <validVar> + "<invalid@email.com>" + <another_valid>',
|
code: 'return <validVar> + "<invalid@email.com>" + <another_valid>',
|
||||||
params: {
|
blockData: {
|
||||||
validVar: 'hello',
|
'block-1': 'hello',
|
||||||
another_valid: 'world',
|
'block-2': 'world',
|
||||||
|
},
|
||||||
|
blockNameMapping: {
|
||||||
|
validvar: 'block-1',
|
||||||
|
another_valid: 'block-2',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -321,28 +354,22 @@ describe('Function Execute API Route', () => {
|
|||||||
it.concurrent(
|
it.concurrent(
|
||||||
'should handle Gmail webhook data with email addresses containing angle brackets',
|
'should handle Gmail webhook data with email addresses containing angle brackets',
|
||||||
async () => {
|
async () => {
|
||||||
const gmailData = {
|
const emailData = {
|
||||||
email: {
|
id: '123',
|
||||||
id: '123',
|
from: 'Waleed Latif <waleed@sim.ai>',
|
||||||
from: 'Waleed Latif <waleed@sim.ai>',
|
to: 'User <user@example.com>',
|
||||||
to: 'User <user@example.com>',
|
subject: 'Test Email',
|
||||||
subject: 'Test Email',
|
bodyText: 'Hello world',
|
||||||
bodyText: 'Hello world',
|
|
||||||
},
|
|
||||||
rawEmail: {
|
|
||||||
id: '123',
|
|
||||||
payload: {
|
|
||||||
headers: [
|
|
||||||
{ name: 'From', value: 'Waleed Latif <waleed@sim.ai>' },
|
|
||||||
{ name: 'To', value: 'User <user@example.com>' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <email>',
|
code: 'return <email>',
|
||||||
params: gmailData,
|
blockData: {
|
||||||
|
'block-email': emailData,
|
||||||
|
},
|
||||||
|
blockNameMapping: {
|
||||||
|
email: 'block-email',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await POST(req)
|
const response = await POST(req)
|
||||||
@@ -356,17 +383,20 @@ describe('Function Execute API Route', () => {
|
|||||||
it.concurrent(
|
it.concurrent(
|
||||||
'should properly serialize complex email objects with special characters',
|
'should properly serialize complex email objects with special characters',
|
||||||
async () => {
|
async () => {
|
||||||
const complexEmailData = {
|
const emailData = {
|
||||||
email: {
|
from: 'Test User <test@example.com>',
|
||||||
from: 'Test User <test@example.com>',
|
bodyHtml: '<div>HTML content with "quotes" and \'apostrophes\'</div>',
|
||||||
bodyHtml: '<div>HTML content with "quotes" and \'apostrophes\'</div>',
|
bodyText: 'Text with\nnewlines\tand\ttabs',
|
||||||
bodyText: 'Text with\nnewlines\tand\ttabs',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <email>',
|
code: 'return <email>',
|
||||||
params: complexEmailData,
|
blockData: {
|
||||||
|
'block-email': emailData,
|
||||||
|
},
|
||||||
|
blockNameMapping: {
|
||||||
|
email: 'block-email',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await POST(req)
|
const response = await POST(req)
|
||||||
@@ -519,18 +549,23 @@ describe('Function Execute API Route', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should handle JSON serialization edge cases', async () => {
|
it.concurrent('should handle JSON serialization edge cases', async () => {
|
||||||
|
const complexData = {
|
||||||
|
special: 'chars"with\'quotes',
|
||||||
|
unicode: '🎉 Unicode content',
|
||||||
|
nested: {
|
||||||
|
deep: {
|
||||||
|
value: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <complexData>',
|
code: 'return <complexData>',
|
||||||
params: {
|
blockData: {
|
||||||
complexData: {
|
'block-complex': complexData,
|
||||||
special: 'chars"with\'quotes',
|
},
|
||||||
unicode: '🎉 Unicode content',
|
blockNameMapping: {
|
||||||
nested: {
|
complexdata: 'block-complex',
|
||||||
deep: {
|
|
||||||
value: 'test',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
|
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { executeInE2B } from '@/lib/execution/e2b'
|
import { executeInE2B } from '@/lib/execution/e2b'
|
||||||
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
|
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
|
||||||
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
||||||
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
|
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
|
||||||
|
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
|
||||||
import {
|
import {
|
||||||
createEnvVarPattern,
|
createEnvVarPattern,
|
||||||
createWorkflowVariablePattern,
|
createWorkflowVariablePattern,
|
||||||
resolveEnvVarReferences,
|
|
||||||
} from '@/executor/utils/reference-validation'
|
} from '@/executor/utils/reference-validation'
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
export const runtime = 'nodejs'
|
export const runtime = 'nodejs'
|
||||||
@@ -18,8 +19,8 @@ export const MAX_DURATION = 210
|
|||||||
|
|
||||||
const logger = createLogger('FunctionExecuteAPI')
|
const logger = createLogger('FunctionExecuteAPI')
|
||||||
|
|
||||||
const E2B_JS_WRAPPER_LINES = 3 // Lines before user code: ';(async () => {', ' try {', ' const __sim_result = await (async () => {'
|
const E2B_JS_WRAPPER_LINES = 3
|
||||||
const E2B_PYTHON_WRAPPER_LINES = 1 // Lines before user code: 'def __sim_main__():'
|
const E2B_PYTHON_WRAPPER_LINES = 1
|
||||||
|
|
||||||
type TypeScriptModule = typeof import('typescript')
|
type TypeScriptModule = typeof import('typescript')
|
||||||
|
|
||||||
@@ -134,33 +135,21 @@ function extractEnhancedError(
|
|||||||
if (error.stack) {
|
if (error.stack) {
|
||||||
enhanced.stack = error.stack
|
enhanced.stack = error.stack
|
||||||
|
|
||||||
// Parse stack trace to extract line and column information
|
|
||||||
// Handle both compilation errors and runtime errors
|
|
||||||
const stackLines: string[] = error.stack.split('\n')
|
const stackLines: string[] = error.stack.split('\n')
|
||||||
|
|
||||||
for (const line of stackLines) {
|
for (const line of stackLines) {
|
||||||
// Pattern 1: Compilation errors - "user-function.js:6"
|
|
||||||
let match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
let match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
||||||
|
|
||||||
// Pattern 2: Runtime errors - "at user-function.js:5:12"
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
match = line.match(/at\s+user-function\.js:(\d+):(\d+)/)
|
match = line.match(/at\s+user-function\.js:(\d+):(\d+)/)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pattern 3: Generic patterns for any line containing our filename
|
|
||||||
if (!match) {
|
|
||||||
match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const stackLine = Number.parseInt(match[1], 10)
|
const stackLine = Number.parseInt(match[1], 10)
|
||||||
const stackColumn = match[2] ? Number.parseInt(match[2], 10) : undefined
|
const stackColumn = match[2] ? Number.parseInt(match[2], 10) : undefined
|
||||||
|
|
||||||
// Adjust line number to account for wrapper code
|
|
||||||
// The user code starts at a specific line in our wrapper
|
|
||||||
const adjustedLine = stackLine - userCodeStartLine + 1
|
const adjustedLine = stackLine - userCodeStartLine + 1
|
||||||
|
|
||||||
// Check if this is a syntax error in wrapper code caused by incomplete user code
|
|
||||||
const isWrapperSyntaxError =
|
const isWrapperSyntaxError =
|
||||||
stackLine > userCodeStartLine &&
|
stackLine > userCodeStartLine &&
|
||||||
error.name === 'SyntaxError' &&
|
error.name === 'SyntaxError' &&
|
||||||
@@ -168,7 +157,6 @@ function extractEnhancedError(
|
|||||||
error.message.includes('Unexpected end of input'))
|
error.message.includes('Unexpected end of input'))
|
||||||
|
|
||||||
if (isWrapperSyntaxError && userCode) {
|
if (isWrapperSyntaxError && userCode) {
|
||||||
// Map wrapper syntax errors to the last line of user code
|
|
||||||
const codeLines = userCode.split('\n')
|
const codeLines = userCode.split('\n')
|
||||||
const lastUserLine = codeLines.length
|
const lastUserLine = codeLines.length
|
||||||
enhanced.line = lastUserLine
|
enhanced.line = lastUserLine
|
||||||
@@ -181,7 +169,6 @@ function extractEnhancedError(
|
|||||||
enhanced.line = adjustedLine
|
enhanced.line = adjustedLine
|
||||||
enhanced.column = stackColumn
|
enhanced.column = stackColumn
|
||||||
|
|
||||||
// Extract the actual line content from user code
|
|
||||||
if (userCode) {
|
if (userCode) {
|
||||||
const codeLines = userCode.split('\n')
|
const codeLines = userCode.split('\n')
|
||||||
if (adjustedLine <= codeLines.length) {
|
if (adjustedLine <= codeLines.length) {
|
||||||
@@ -192,7 +179,6 @@ function extractEnhancedError(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stackLine <= userCodeStartLine) {
|
if (stackLine <= userCodeStartLine) {
|
||||||
// Error is in wrapper code itself
|
|
||||||
enhanced.line = stackLine
|
enhanced.line = stackLine
|
||||||
enhanced.column = stackColumn
|
enhanced.column = stackColumn
|
||||||
break
|
break
|
||||||
@@ -200,7 +186,6 @@ function extractEnhancedError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up stack trace to show user-relevant information
|
|
||||||
const cleanedStackLines: string[] = stackLines
|
const cleanedStackLines: string[] = stackLines
|
||||||
.filter(
|
.filter(
|
||||||
(line: string) =>
|
(line: string) =>
|
||||||
@@ -214,9 +199,6 @@ function extractEnhancedError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep original message without adding error type prefix
|
|
||||||
// The error type will be added later in createUserFriendlyErrorMessage
|
|
||||||
|
|
||||||
return enhanced
|
return enhanced
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +213,6 @@ function formatE2BError(
|
|||||||
userCode: string,
|
userCode: string,
|
||||||
prologueLineCount: number
|
prologueLineCount: number
|
||||||
): { formattedError: string; cleanedOutput: string } {
|
): { formattedError: string; cleanedOutput: string } {
|
||||||
// Calculate line offset based on language and prologue
|
|
||||||
const wrapperLines =
|
const wrapperLines =
|
||||||
language === CodeLanguage.Python ? E2B_PYTHON_WRAPPER_LINES : E2B_JS_WRAPPER_LINES
|
language === CodeLanguage.Python ? E2B_PYTHON_WRAPPER_LINES : E2B_JS_WRAPPER_LINES
|
||||||
const totalOffset = prologueLineCount + wrapperLines
|
const totalOffset = prologueLineCount + wrapperLines
|
||||||
@@ -241,27 +222,20 @@ function formatE2BError(
|
|||||||
let cleanErrorMsg = ''
|
let cleanErrorMsg = ''
|
||||||
|
|
||||||
if (language === CodeLanguage.Python) {
|
if (language === CodeLanguage.Python) {
|
||||||
// Python error format: "Cell In[X], line Y" followed by error details
|
|
||||||
// Extract line number from the Cell reference
|
|
||||||
const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/)
|
const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/)
|
||||||
if (cellMatch) {
|
if (cellMatch) {
|
||||||
const originalLine = Number.parseInt(cellMatch[1], 10)
|
const originalLine = Number.parseInt(cellMatch[1], 10)
|
||||||
userLine = originalLine - totalOffset
|
userLine = originalLine - totalOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract clean error message from the error string
|
|
||||||
// Remove file references like "(detected at line X) (file.py, line Y)"
|
|
||||||
cleanErrorMsg = errorMessage
|
cleanErrorMsg = errorMessage
|
||||||
.replace(/\s*\(detected at line \d+\)/g, '')
|
.replace(/\s*\(detected at line \d+\)/g, '')
|
||||||
.replace(/\s*\([^)]+\.py, line \d+\)/g, '')
|
.replace(/\s*\([^)]+\.py, line \d+\)/g, '')
|
||||||
.trim()
|
.trim()
|
||||||
} else if (language === CodeLanguage.JavaScript) {
|
} else if (language === CodeLanguage.JavaScript) {
|
||||||
// JavaScript error format from E2B: "SyntaxError: /path/file.ts: Message. (line:col)\n\n 9 | ..."
|
|
||||||
// First, extract the error type and message from the first line
|
|
||||||
const firstLineEnd = errorMessage.indexOf('\n')
|
const firstLineEnd = errorMessage.indexOf('\n')
|
||||||
const firstLine = firstLineEnd > 0 ? errorMessage.substring(0, firstLineEnd) : errorMessage
|
const firstLine = firstLineEnd > 0 ? errorMessage.substring(0, firstLineEnd) : errorMessage
|
||||||
|
|
||||||
// Parse: "SyntaxError: /home/user/index.ts: Missing semicolon. (11:9)"
|
|
||||||
const jsErrorMatch = firstLine.match(/^(\w+Error):\s*[^:]+:\s*([^(]+)\.\s*\((\d+):(\d+)\)/)
|
const jsErrorMatch = firstLine.match(/^(\w+Error):\s*[^:]+:\s*([^(]+)\.\s*\((\d+):(\d+)\)/)
|
||||||
if (jsErrorMatch) {
|
if (jsErrorMatch) {
|
||||||
cleanErrorType = jsErrorMatch[1]
|
cleanErrorType = jsErrorMatch[1]
|
||||||
@@ -269,13 +243,11 @@ function formatE2BError(
|
|||||||
const originalLine = Number.parseInt(jsErrorMatch[3], 10)
|
const originalLine = Number.parseInt(jsErrorMatch[3], 10)
|
||||||
userLine = originalLine - totalOffset
|
userLine = originalLine - totalOffset
|
||||||
} else {
|
} else {
|
||||||
// Fallback: look for line number in the arrow pointer line (> 11 |)
|
|
||||||
const arrowMatch = errorMessage.match(/^>\s*(\d+)\s*\|/m)
|
const arrowMatch = errorMessage.match(/^>\s*(\d+)\s*\|/m)
|
||||||
if (arrowMatch) {
|
if (arrowMatch) {
|
||||||
const originalLine = Number.parseInt(arrowMatch[1], 10)
|
const originalLine = Number.parseInt(arrowMatch[1], 10)
|
||||||
userLine = originalLine - totalOffset
|
userLine = originalLine - totalOffset
|
||||||
}
|
}
|
||||||
// Try to extract error type and message
|
|
||||||
const errorMatch = firstLine.match(/^(\w+Error):\s*(.+)/)
|
const errorMatch = firstLine.match(/^(\w+Error):\s*(.+)/)
|
||||||
if (errorMatch) {
|
if (errorMatch) {
|
||||||
cleanErrorType = errorMatch[1]
|
cleanErrorType = errorMatch[1]
|
||||||
@@ -289,13 +261,11 @@ function formatE2BError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the final clean error message
|
|
||||||
const finalErrorMsg =
|
const finalErrorMsg =
|
||||||
cleanErrorType && cleanErrorMsg
|
cleanErrorType && cleanErrorMsg
|
||||||
? `${cleanErrorType}: ${cleanErrorMsg}`
|
? `${cleanErrorType}: ${cleanErrorMsg}`
|
||||||
: cleanErrorMsg || errorMessage
|
: cleanErrorMsg || errorMessage
|
||||||
|
|
||||||
// Format with line number if available
|
|
||||||
let formattedError = finalErrorMsg
|
let formattedError = finalErrorMsg
|
||||||
if (userLine && userLine > 0) {
|
if (userLine && userLine > 0) {
|
||||||
const codeLines = userCode.split('\n')
|
const codeLines = userCode.split('\n')
|
||||||
@@ -311,7 +281,6 @@ function formatE2BError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For stdout, just return the clean error message without the full traceback
|
|
||||||
const cleanedOutput = finalErrorMsg
|
const cleanedOutput = finalErrorMsg
|
||||||
|
|
||||||
return { formattedError, cleanedOutput }
|
return { formattedError, cleanedOutput }
|
||||||
@@ -327,7 +296,6 @@ function createUserFriendlyErrorMessage(
|
|||||||
): string {
|
): string {
|
||||||
let errorMessage = enhanced.message
|
let errorMessage = enhanced.message
|
||||||
|
|
||||||
// Add line information if available
|
|
||||||
if (enhanced.line !== undefined) {
|
if (enhanced.line !== undefined) {
|
||||||
let lineInfo = `Line ${enhanced.line}`
|
let lineInfo = `Line ${enhanced.line}`
|
||||||
|
|
||||||
@@ -338,18 +306,14 @@ function createUserFriendlyErrorMessage(
|
|||||||
|
|
||||||
errorMessage = `${lineInfo} - ${errorMessage}`
|
errorMessage = `${lineInfo} - ${errorMessage}`
|
||||||
} else {
|
} else {
|
||||||
// If no line number, try to extract it from stack trace for display
|
|
||||||
if (enhanced.stack) {
|
if (enhanced.stack) {
|
||||||
const stackMatch = enhanced.stack.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
const stackMatch = enhanced.stack.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
||||||
if (stackMatch) {
|
if (stackMatch) {
|
||||||
const line = Number.parseInt(stackMatch[1], 10)
|
const line = Number.parseInt(stackMatch[1], 10)
|
||||||
let lineInfo = `Line ${line}`
|
let lineInfo = `Line ${line}`
|
||||||
|
|
||||||
// Try to get line content if we have userCode
|
|
||||||
if (userCode) {
|
if (userCode) {
|
||||||
const codeLines = userCode.split('\n')
|
const codeLines = userCode.split('\n')
|
||||||
// Note: stackMatch gives us VM line number, need to adjust
|
|
||||||
// This is a fallback case, so we might not have perfect line mapping
|
|
||||||
if (line <= codeLines.length) {
|
if (line <= codeLines.length) {
|
||||||
const lineContent = codeLines[line - 1]?.trim()
|
const lineContent = codeLines[line - 1]?.trim()
|
||||||
if (lineContent) {
|
if (lineContent) {
|
||||||
@@ -363,7 +327,6 @@ function createUserFriendlyErrorMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add error type prefix with consistent naming
|
|
||||||
if (enhanced.name !== 'Error') {
|
if (enhanced.name !== 'Error') {
|
||||||
const errorTypePrefix =
|
const errorTypePrefix =
|
||||||
enhanced.name === 'SyntaxError'
|
enhanced.name === 'SyntaxError'
|
||||||
@@ -374,7 +337,6 @@ function createUserFriendlyErrorMessage(
|
|||||||
? 'Reference Error'
|
? 'Reference Error'
|
||||||
: enhanced.name
|
: enhanced.name
|
||||||
|
|
||||||
// Only add prefix if not already present
|
|
||||||
if (!errorMessage.toLowerCase().includes(errorTypePrefix.toLowerCase())) {
|
if (!errorMessage.toLowerCase().includes(errorTypePrefix.toLowerCase())) {
|
||||||
errorMessage = `${errorTypePrefix}: ${errorMessage}`
|
errorMessage = `${errorTypePrefix}: ${errorMessage}`
|
||||||
}
|
}
|
||||||
@@ -383,9 +345,6 @@ function createUserFriendlyErrorMessage(
|
|||||||
return errorMessage
|
return errorMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves workflow variables with <variable.name> syntax
|
|
||||||
*/
|
|
||||||
function resolveWorkflowVariables(
|
function resolveWorkflowVariables(
|
||||||
code: string,
|
code: string,
|
||||||
workflowVariables: Record<string, any>,
|
workflowVariables: Record<string, any>,
|
||||||
@@ -405,39 +364,35 @@ function resolveWorkflowVariables(
|
|||||||
while ((match = regex.exec(code)) !== null) {
|
while ((match = regex.exec(code)) !== null) {
|
||||||
const variableName = match[1].trim()
|
const variableName = match[1].trim()
|
||||||
|
|
||||||
// Find the variable by name (workflowVariables is indexed by ID, values are variable objects)
|
|
||||||
const foundVariable = Object.entries(workflowVariables).find(
|
const foundVariable = Object.entries(workflowVariables).find(
|
||||||
([_, variable]) => normalizeName(variable.name || '') === variableName
|
([_, variable]) => normalizeName(variable.name || '') === variableName
|
||||||
)
|
)
|
||||||
|
|
||||||
let variableValue: unknown = ''
|
if (!foundVariable) {
|
||||||
if (foundVariable) {
|
const availableVars = Object.values(workflowVariables)
|
||||||
const variable = foundVariable[1]
|
.map((v) => v.name)
|
||||||
variableValue = variable.value
|
.filter(Boolean)
|
||||||
|
throw new Error(
|
||||||
|
`Variable "${variableName}" doesn't exist.` +
|
||||||
|
(availableVars.length > 0 ? ` Available: ${availableVars.join(', ')}` : '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (variable.value !== undefined && variable.value !== null) {
|
const variable = foundVariable[1]
|
||||||
|
let variableValue: unknown = variable.value
|
||||||
|
|
||||||
|
if (variable.value !== undefined && variable.value !== null) {
|
||||||
|
const type = variable.type === 'string' ? 'plain' : variable.type
|
||||||
|
|
||||||
|
if (type === 'number') {
|
||||||
|
variableValue = Number(variableValue)
|
||||||
|
} else if (type === 'boolean') {
|
||||||
|
variableValue = variableValue === 'true' || variableValue === true
|
||||||
|
} else if (type === 'json' && typeof variableValue === 'string') {
|
||||||
try {
|
try {
|
||||||
// Handle 'string' type the same as 'plain' for backward compatibility
|
variableValue = JSON.parse(variableValue)
|
||||||
const type = variable.type === 'string' ? 'plain' : variable.type
|
|
||||||
|
|
||||||
// For plain text, use exactly what's entered without modifications
|
|
||||||
if (type === 'plain' && typeof variableValue === 'string') {
|
|
||||||
// Use as-is for plain text
|
|
||||||
} else if (type === 'number') {
|
|
||||||
variableValue = Number(variableValue)
|
|
||||||
} else if (type === 'boolean') {
|
|
||||||
variableValue = variableValue === 'true' || variableValue === true
|
|
||||||
} else if (type === 'json') {
|
|
||||||
try {
|
|
||||||
variableValue =
|
|
||||||
typeof variableValue === 'string' ? JSON.parse(variableValue) : variableValue
|
|
||||||
} catch {
|
|
||||||
// Keep original value if JSON parsing fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback to original value on error
|
// Keep as-is
|
||||||
variableValue = variable.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,11 +405,9 @@ function resolveWorkflowVariables(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process replacements in reverse order to maintain correct indices
|
|
||||||
for (let i = replacements.length - 1; i >= 0; i--) {
|
for (let i = replacements.length - 1; i >= 0; i--) {
|
||||||
const { match: matchStr, index, variableName, variableValue } = replacements[i]
|
const { match: matchStr, index, variableName, variableValue } = replacements[i]
|
||||||
|
|
||||||
// Use variable reference approach
|
|
||||||
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||||
contextVariables[safeVarName] = variableValue
|
contextVariables[safeVarName] = variableValue
|
||||||
resolvedCode =
|
resolvedCode =
|
||||||
@@ -464,9 +417,6 @@ function resolveWorkflowVariables(
|
|||||||
return resolvedCode
|
return resolvedCode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves environment variables with {{var_name}} syntax
|
|
||||||
*/
|
|
||||||
function resolveEnvironmentVariables(
|
function resolveEnvironmentVariables(
|
||||||
code: string,
|
code: string,
|
||||||
params: Record<string, any>,
|
params: Record<string, any>,
|
||||||
@@ -482,32 +432,28 @@ function resolveEnvironmentVariables(
|
|||||||
|
|
||||||
const resolverVars: Record<string, string> = {}
|
const resolverVars: Record<string, string> = {}
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value) {
|
if (value !== undefined && value !== null) {
|
||||||
resolverVars[key] = String(value)
|
resolverVars[key] = String(value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Object.entries(envVars).forEach(([key, value]) => {
|
Object.entries(envVars).forEach(([key, value]) => {
|
||||||
if (value) {
|
if (value !== undefined && value !== null) {
|
||||||
resolverVars[key] = value
|
resolverVars[key] = value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
while ((match = regex.exec(code)) !== null) {
|
while ((match = regex.exec(code)) !== null) {
|
||||||
const varName = match[1].trim()
|
const varName = match[1].trim()
|
||||||
const resolved = resolveEnvVarReferences(match[0], resolverVars, {
|
|
||||||
allowEmbedded: true,
|
if (!(varName in resolverVars)) {
|
||||||
resolveExactMatch: true,
|
continue
|
||||||
trimKeys: true,
|
}
|
||||||
onMissing: 'empty',
|
|
||||||
deep: false,
|
|
||||||
})
|
|
||||||
const varValue =
|
|
||||||
typeof resolved === 'string' ? resolved : resolved == null ? '' : String(resolved)
|
|
||||||
replacements.push({
|
replacements.push({
|
||||||
match: match[0],
|
match: match[0],
|
||||||
index: match.index,
|
index: match.index,
|
||||||
varName,
|
varName,
|
||||||
varValue: String(varValue),
|
varValue: resolverVars[varName],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,64 +469,59 @@ function resolveEnvironmentVariables(
|
|||||||
return resolvedCode
|
return resolvedCode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves tags with <tag_name> syntax (including nested paths like <block.response.data>)
|
|
||||||
*/
|
|
||||||
function resolveTagVariables(
|
function resolveTagVariables(
|
||||||
code: string,
|
code: string,
|
||||||
params: Record<string, any>,
|
blockData: Record<string, unknown>,
|
||||||
blockData: Record<string, any>,
|
|
||||||
blockNameMapping: Record<string, string>,
|
blockNameMapping: Record<string, string>,
|
||||||
contextVariables: Record<string, any>
|
blockOutputSchemas: Record<string, OutputSchema>,
|
||||||
|
contextVariables: Record<string, unknown>,
|
||||||
|
language = 'javascript'
|
||||||
): string {
|
): string {
|
||||||
let resolvedCode = code
|
let resolvedCode = code
|
||||||
|
const undefinedLiteral = language === 'python' ? 'None' : 'undefined'
|
||||||
|
|
||||||
const tagPattern = new RegExp(
|
const tagPattern = new RegExp(
|
||||||
`${REFERENCE.START}([a-zA-Z_][a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])${REFERENCE.END}`,
|
`${REFERENCE.START}([a-zA-Z_](?:[a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])?)${REFERENCE.END}`,
|
||||||
'g'
|
'g'
|
||||||
)
|
)
|
||||||
const tagMatches = resolvedCode.match(tagPattern) || []
|
const tagMatches = resolvedCode.match(tagPattern) || []
|
||||||
|
|
||||||
for (const match of tagMatches) {
|
for (const match of tagMatches) {
|
||||||
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
|
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
|
||||||
|
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
|
||||||
|
const blockName = pathParts[0]
|
||||||
|
const fieldPath = pathParts.slice(1)
|
||||||
|
|
||||||
// Handle nested paths like "getrecord.response.data" or "function1.response.result"
|
const result = resolveBlockReference(blockName, fieldPath, {
|
||||||
// First try params, then blockData directly, then try with block name mapping
|
blockNameMapping,
|
||||||
let tagValue = getNestedValue(params, tagName) || getNestedValue(blockData, tagName) || ''
|
blockData,
|
||||||
|
blockOutputSchemas,
|
||||||
|
})
|
||||||
|
|
||||||
// If not found and the path starts with a block name, try mapping the block name to ID
|
if (!result) {
|
||||||
if (!tagValue && tagName.includes(REFERENCE.PATH_DELIMITER)) {
|
continue
|
||||||
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
|
}
|
||||||
const normalizedBlockName = pathParts[0] // This should already be normalized like "function1"
|
|
||||||
|
|
||||||
// Direct lookup using normalized block name
|
let tagValue = result.value
|
||||||
const blockId = blockNameMapping[normalizedBlockName] ?? null
|
|
||||||
|
|
||||||
if (blockId) {
|
if (tagValue === undefined) {
|
||||||
const remainingPath = pathParts.slice(1).join('.')
|
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), undefinedLiteral)
|
||||||
const fullPath = `${blockId}.${remainingPath}`
|
continue
|
||||||
tagValue = getNestedValue(blockData, fullPath) || ''
|
}
|
||||||
|
|
||||||
|
if (typeof tagValue === 'string') {
|
||||||
|
const trimmed = tagValue.trimStart()
|
||||||
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
tagValue = JSON.parse(tagValue)
|
||||||
|
} catch {
|
||||||
|
// Keep as string if not valid JSON
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the value is a stringified JSON, parse it back to object
|
const safeVarName = `__tag_${tagName.replace(/_/g, '_1').replace(/\./g, '_0')}`
|
||||||
if (
|
|
||||||
typeof tagValue === 'string' &&
|
|
||||||
tagValue.length > 100 &&
|
|
||||||
(tagValue.startsWith('{') || tagValue.startsWith('['))
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
tagValue = JSON.parse(tagValue)
|
|
||||||
} catch (e) {
|
|
||||||
// Keep as string if parsing fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instead of injecting large JSON directly, create a variable reference
|
|
||||||
const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
|
||||||
contextVariables[safeVarName] = tagValue
|
contextVariables[safeVarName] = tagValue
|
||||||
|
|
||||||
// Replace the template with a variable reference
|
|
||||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,44 +537,31 @@ function resolveTagVariables(
|
|||||||
*/
|
*/
|
||||||
function resolveCodeVariables(
|
function resolveCodeVariables(
|
||||||
code: string,
|
code: string,
|
||||||
params: Record<string, any>,
|
params: Record<string, unknown>,
|
||||||
envVars: Record<string, string> = {},
|
envVars: Record<string, string> = {},
|
||||||
blockData: Record<string, any> = {},
|
blockData: Record<string, unknown> = {},
|
||||||
blockNameMapping: Record<string, string> = {},
|
blockNameMapping: Record<string, string> = {},
|
||||||
workflowVariables: Record<string, any> = {}
|
blockOutputSchemas: Record<string, OutputSchema> = {},
|
||||||
): { resolvedCode: string; contextVariables: Record<string, any> } {
|
workflowVariables: Record<string, unknown> = {},
|
||||||
|
language = 'javascript'
|
||||||
|
): { resolvedCode: string; contextVariables: Record<string, unknown> } {
|
||||||
let resolvedCode = code
|
let resolvedCode = code
|
||||||
const contextVariables: Record<string, any> = {}
|
const contextVariables: Record<string, unknown> = {}
|
||||||
|
|
||||||
// Resolve workflow variables with <variable.name> syntax first
|
|
||||||
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
|
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
|
||||||
|
|
||||||
// Resolve environment variables with {{var_name}} syntax
|
|
||||||
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
|
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
|
||||||
|
|
||||||
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
|
|
||||||
resolvedCode = resolveTagVariables(
|
resolvedCode = resolveTagVariables(
|
||||||
resolvedCode,
|
resolvedCode,
|
||||||
params,
|
|
||||||
blockData,
|
blockData,
|
||||||
blockNameMapping,
|
blockNameMapping,
|
||||||
contextVariables
|
blockOutputSchemas,
|
||||||
|
contextVariables,
|
||||||
|
language
|
||||||
)
|
)
|
||||||
|
|
||||||
return { resolvedCode, contextVariables }
|
return { resolvedCode, contextVariables }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get nested value from object using dot notation path
|
|
||||||
*/
|
|
||||||
function getNestedValue(obj: any, path: string): any {
|
|
||||||
if (!obj || !path) return undefined
|
|
||||||
|
|
||||||
return path.split('.').reduce((current, key) => {
|
|
||||||
return current && typeof current === 'object' ? current[key] : undefined
|
|
||||||
}, obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove one trailing newline from stdout
|
* Remove one trailing newline from stdout
|
||||||
* This handles the common case where print() or console.log() adds a trailing \n
|
* This handles the common case where print() or console.log() adds a trailing \n
|
||||||
@@ -654,6 +582,12 @@ export async function POST(req: NextRequest) {
|
|||||||
let resolvedCode = '' // Store resolved code for error reporting
|
let resolvedCode = '' // Store resolved code for error reporting
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(req)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized function execution attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
|
|
||||||
const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')
|
const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')
|
||||||
@@ -666,12 +600,12 @@ export async function POST(req: NextRequest) {
|
|||||||
envVars = {},
|
envVars = {},
|
||||||
blockData = {},
|
blockData = {},
|
||||||
blockNameMapping = {},
|
blockNameMapping = {},
|
||||||
|
blockOutputSchemas = {},
|
||||||
workflowVariables = {},
|
workflowVariables = {},
|
||||||
workflowId,
|
workflowId,
|
||||||
isCustomTool = false,
|
isCustomTool = false,
|
||||||
} = body
|
} = body
|
||||||
|
|
||||||
// Extract internal parameters that shouldn't be passed to the execution context
|
|
||||||
const executionParams = { ...params }
|
const executionParams = { ...params }
|
||||||
executionParams._context = undefined
|
executionParams._context = undefined
|
||||||
|
|
||||||
@@ -683,21 +617,21 @@ export async function POST(req: NextRequest) {
|
|||||||
isCustomTool,
|
isCustomTool,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Resolve variables in the code with workflow environment variables
|
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
|
||||||
|
|
||||||
const codeResolution = resolveCodeVariables(
|
const codeResolution = resolveCodeVariables(
|
||||||
code,
|
code,
|
||||||
executionParams,
|
executionParams,
|
||||||
envVars,
|
envVars,
|
||||||
blockData,
|
blockData,
|
||||||
blockNameMapping,
|
blockNameMapping,
|
||||||
workflowVariables
|
blockOutputSchemas,
|
||||||
|
workflowVariables,
|
||||||
|
lang
|
||||||
)
|
)
|
||||||
resolvedCode = codeResolution.resolvedCode
|
resolvedCode = codeResolution.resolvedCode
|
||||||
const contextVariables = codeResolution.contextVariables
|
const contextVariables = codeResolution.contextVariables
|
||||||
|
|
||||||
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
|
|
||||||
|
|
||||||
// Extract imports once for JavaScript code (reuse later to avoid double extraction)
|
|
||||||
let jsImports = ''
|
let jsImports = ''
|
||||||
let jsRemainingCode = resolvedCode
|
let jsRemainingCode = resolvedCode
|
||||||
let hasImports = false
|
let hasImports = false
|
||||||
@@ -707,31 +641,22 @@ export async function POST(req: NextRequest) {
|
|||||||
jsImports = extractionResult.imports
|
jsImports = extractionResult.imports
|
||||||
jsRemainingCode = extractionResult.remainingCode
|
jsRemainingCode = extractionResult.remainingCode
|
||||||
|
|
||||||
// Check for ES6 imports or CommonJS require statements
|
|
||||||
// ES6 imports are extracted by the TypeScript parser
|
|
||||||
// Also check for require() calls which indicate external dependencies
|
|
||||||
const hasRequireStatements = /require\s*\(\s*['"`]/.test(resolvedCode)
|
const hasRequireStatements = /require\s*\(\s*['"`]/.test(resolvedCode)
|
||||||
hasImports = jsImports.trim().length > 0 || hasRequireStatements
|
hasImports = jsImports.trim().length > 0 || hasRequireStatements
|
||||||
}
|
}
|
||||||
|
|
||||||
// Python always requires E2B
|
|
||||||
if (lang === CodeLanguage.Python && !isE2bEnabled) {
|
if (lang === CodeLanguage.Python && !isE2bEnabled) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.'
|
'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JavaScript with imports requires E2B
|
|
||||||
if (lang === CodeLanguage.JavaScript && hasImports && !isE2bEnabled) {
|
if (lang === CodeLanguage.JavaScript && hasImports && !isE2bEnabled) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'JavaScript code with import statements requires E2B to be enabled. Please remove the import statements, or contact your administrator to enable E2B.'
|
'JavaScript code with import statements requires E2B to be enabled. Please remove the import statements, or contact your administrator to enable E2B.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use E2B if:
|
|
||||||
// - E2B is enabled AND
|
|
||||||
// - Not a custom tool AND
|
|
||||||
// - (Python OR JavaScript with imports)
|
|
||||||
const useE2B =
|
const useE2B =
|
||||||
isE2bEnabled &&
|
isE2bEnabled &&
|
||||||
!isCustomTool &&
|
!isCustomTool &&
|
||||||
@@ -744,13 +669,10 @@ export async function POST(req: NextRequest) {
|
|||||||
language: lang,
|
language: lang,
|
||||||
})
|
})
|
||||||
let prologue = ''
|
let prologue = ''
|
||||||
const epilogue = ''
|
|
||||||
|
|
||||||
if (lang === CodeLanguage.JavaScript) {
|
if (lang === CodeLanguage.JavaScript) {
|
||||||
// Track prologue lines for error adjustment
|
|
||||||
let prologueLineCount = 0
|
let prologueLineCount = 0
|
||||||
|
|
||||||
// Reuse the imports we already extracted earlier
|
|
||||||
const imports = jsImports
|
const imports = jsImports
|
||||||
const remainingCode = jsRemainingCode
|
const remainingCode = jsRemainingCode
|
||||||
|
|
||||||
@@ -765,7 +687,11 @@ export async function POST(req: NextRequest) {
|
|||||||
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
|
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
for (const [k, v] of Object.entries(contextVariables)) {
|
for (const [k, v] of Object.entries(contextVariables)) {
|
||||||
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
|
if (v === undefined) {
|
||||||
|
prologue += `const ${k} = undefined;\n`
|
||||||
|
} else {
|
||||||
|
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
|
||||||
|
}
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,7 +708,7 @@ export async function POST(req: NextRequest) {
|
|||||||
' }',
|
' }',
|
||||||
'})();',
|
'})();',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const codeForE2B = importSection + prologue + wrapped + epilogue
|
const codeForE2B = importSection + prologue + wrapped
|
||||||
|
|
||||||
const execStart = Date.now()
|
const execStart = Date.now()
|
||||||
const {
|
const {
|
||||||
@@ -804,7 +730,6 @@ export async function POST(req: NextRequest) {
|
|||||||
error: e2bError,
|
error: e2bError,
|
||||||
})
|
})
|
||||||
|
|
||||||
// If there was an execution error, format it properly
|
|
||||||
if (e2bError) {
|
if (e2bError) {
|
||||||
const { formattedError, cleanedOutput } = formatE2BError(
|
const { formattedError, cleanedOutput } = formatE2BError(
|
||||||
e2bError,
|
e2bError,
|
||||||
@@ -828,7 +753,7 @@ export async function POST(req: NextRequest) {
|
|||||||
output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime },
|
output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Track prologue lines for error adjustment
|
|
||||||
let prologueLineCount = 0
|
let prologueLineCount = 0
|
||||||
prologue += 'import json\n'
|
prologue += 'import json\n'
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
@@ -837,7 +762,11 @@ export async function POST(req: NextRequest) {
|
|||||||
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
|
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
for (const [k, v] of Object.entries(contextVariables)) {
|
for (const [k, v] of Object.entries(contextVariables)) {
|
||||||
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
|
if (v === undefined) {
|
||||||
|
prologue += `${k} = None\n`
|
||||||
|
} else {
|
||||||
|
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
|
||||||
|
}
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
}
|
}
|
||||||
const wrapped = [
|
const wrapped = [
|
||||||
@@ -846,7 +775,7 @@ export async function POST(req: NextRequest) {
|
|||||||
'__sim_result__ = __sim_main__()',
|
'__sim_result__ = __sim_main__()',
|
||||||
"print('__SIM_RESULT__=' + json.dumps(__sim_result__))",
|
"print('__SIM_RESULT__=' + json.dumps(__sim_result__))",
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const codeForE2B = prologue + wrapped + epilogue
|
const codeForE2B = prologue + wrapped
|
||||||
|
|
||||||
const execStart = Date.now()
|
const execStart = Date.now()
|
||||||
const {
|
const {
|
||||||
@@ -868,7 +797,6 @@ export async function POST(req: NextRequest) {
|
|||||||
error: e2bError,
|
error: e2bError,
|
||||||
})
|
})
|
||||||
|
|
||||||
// If there was an execution error, format it properly
|
|
||||||
if (e2bError) {
|
if (e2bError) {
|
||||||
const { formattedError, cleanedOutput } = formatE2BError(
|
const { formattedError, cleanedOutput } = formatE2BError(
|
||||||
e2bError,
|
e2bError,
|
||||||
@@ -897,7 +825,6 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const wrapperLines = ['(async () => {', ' try {']
|
const wrapperLines = ['(async () => {', ' try {']
|
||||||
if (isCustomTool) {
|
if (isCustomTool) {
|
||||||
wrapperLines.push(' // For custom tools, make parameters directly accessible')
|
|
||||||
Object.keys(executionParams).forEach((key) => {
|
Object.keys(executionParams).forEach((key) => {
|
||||||
wrapperLines.push(` const ${key} = params.${key};`)
|
wrapperLines.push(` const ${key} = params.${key};`)
|
||||||
})
|
})
|
||||||
@@ -931,12 +858,10 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const ivmError = isolatedResult.error
|
const ivmError = isolatedResult.error
|
||||||
// Adjust line number for prepended param destructuring in custom tools
|
|
||||||
let adjustedLine = ivmError.line
|
let adjustedLine = ivmError.line
|
||||||
let adjustedLineContent = ivmError.lineContent
|
let adjustedLineContent = ivmError.lineContent
|
||||||
if (prependedLineCount > 0 && ivmError.line !== undefined) {
|
if (prependedLineCount > 0 && ivmError.line !== undefined) {
|
||||||
adjustedLine = Math.max(1, ivmError.line - prependedLineCount)
|
adjustedLine = Math.max(1, ivmError.line - prependedLineCount)
|
||||||
// Get line content from original user code, not the prepended code
|
|
||||||
const codeLines = resolvedCode.split('\n')
|
const codeLines = resolvedCode.split('\n')
|
||||||
if (adjustedLine <= codeLines.length) {
|
if (adjustedLine <= codeLines.length) {
|
||||||
adjustedLineContent = codeLines[adjustedLine - 1]?.trim()
|
adjustedLineContent = codeLines[adjustedLine - 1]?.trim()
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
||||||
'kb-123',
|
'kb-123',
|
||||||
{
|
{
|
||||||
includeDisabled: false,
|
enabledFilter: undefined,
|
||||||
search: undefined,
|
search: undefined,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -166,7 +166,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should filter disabled documents by default', async () => {
|
it('should return documents with default filter', async () => {
|
||||||
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
||||||
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
||||||
'kb-123',
|
'kb-123',
|
||||||
{
|
{
|
||||||
includeDisabled: false,
|
enabledFilter: undefined,
|
||||||
search: undefined,
|
search: undefined,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -203,7 +203,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should include disabled documents when requested', async () => {
|
it('should filter documents by enabled status when requested', async () => {
|
||||||
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
||||||
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?includeDisabled=true'
|
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?enabledFilter=disabled'
|
||||||
const req = new Request(url, { method: 'GET' }) as any
|
const req = new Request(url, { method: 'GET' }) as any
|
||||||
|
|
||||||
const { GET } = await import('@/app/api/knowledge/[id]/documents/route')
|
const { GET } = await import('@/app/api/knowledge/[id]/documents/route')
|
||||||
@@ -233,7 +233,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
||||||
'kb-123',
|
'kb-123',
|
||||||
{
|
{
|
||||||
includeDisabled: true,
|
enabledFilter: 'disabled',
|
||||||
search: undefined,
|
search: undefined,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -361,8 +361,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
expect(vi.mocked(createSingleDocument)).toHaveBeenCalledWith(
|
expect(vi.mocked(createSingleDocument)).toHaveBeenCalledWith(
|
||||||
validDocumentData,
|
validDocumentData,
|
||||||
'kb-123',
|
'kb-123',
|
||||||
expect.any(String),
|
expect.any(String)
|
||||||
'user-123'
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -470,8 +469,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
expect(vi.mocked(createDocumentRecords)).toHaveBeenCalledWith(
|
expect(vi.mocked(createDocumentRecords)).toHaveBeenCalledWith(
|
||||||
validBulkData.documents,
|
validBulkData.documents,
|
||||||
'kb-123',
|
'kb-123',
|
||||||
expect.any(String),
|
expect.any(String)
|
||||||
'user-123'
|
|
||||||
)
|
)
|
||||||
expect(vi.mocked(processDocumentsWithQueue)).toHaveBeenCalled()
|
expect(vi.mocked(processDocumentsWithQueue)).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from 'zod'
|
|||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import {
|
import {
|
||||||
bulkDocumentOperation,
|
bulkDocumentOperation,
|
||||||
|
bulkDocumentOperationByFilter,
|
||||||
createDocumentRecords,
|
createDocumentRecords,
|
||||||
createSingleDocument,
|
createSingleDocument,
|
||||||
getDocuments,
|
getDocuments,
|
||||||
@@ -57,13 +58,20 @@ const BulkCreateDocumentsSchema = z.object({
|
|||||||
bulk: z.literal(true),
|
bulk: z.literal(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
const BulkUpdateDocumentsSchema = z.object({
|
const BulkUpdateDocumentsSchema = z
|
||||||
operation: z.enum(['enable', 'disable', 'delete']),
|
.object({
|
||||||
documentIds: z
|
operation: z.enum(['enable', 'disable', 'delete']),
|
||||||
.array(z.string())
|
documentIds: z
|
||||||
.min(1, 'At least one document ID is required')
|
.array(z.string())
|
||||||
.max(100, 'Cannot operate on more than 100 documents at once'),
|
.min(1, 'At least one document ID is required')
|
||||||
})
|
.max(100, 'Cannot operate on more than 100 documents at once')
|
||||||
|
.optional(),
|
||||||
|
selectAll: z.boolean().optional(),
|
||||||
|
enabledFilter: z.enum(['all', 'enabled', 'disabled']).optional(),
|
||||||
|
})
|
||||||
|
.refine((data) => data.selectAll || (data.documentIds && data.documentIds.length > 0), {
|
||||||
|
message: 'Either selectAll must be true or documentIds must be provided',
|
||||||
|
})
|
||||||
|
|
||||||
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
@@ -90,14 +98,17 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(req.url)
|
const url = new URL(req.url)
|
||||||
const includeDisabled = url.searchParams.get('includeDisabled') === 'true'
|
const enabledFilter = url.searchParams.get('enabledFilter') as
|
||||||
|
| 'all'
|
||||||
|
| 'enabled'
|
||||||
|
| 'disabled'
|
||||||
|
| null
|
||||||
const search = url.searchParams.get('search') || undefined
|
const search = url.searchParams.get('search') || undefined
|
||||||
const limit = Number.parseInt(url.searchParams.get('limit') || '50')
|
const limit = Number.parseInt(url.searchParams.get('limit') || '50')
|
||||||
const offset = Number.parseInt(url.searchParams.get('offset') || '0')
|
const offset = Number.parseInt(url.searchParams.get('offset') || '0')
|
||||||
const sortByParam = url.searchParams.get('sortBy')
|
const sortByParam = url.searchParams.get('sortBy')
|
||||||
const sortOrderParam = url.searchParams.get('sortOrder')
|
const sortOrderParam = url.searchParams.get('sortOrder')
|
||||||
|
|
||||||
// Validate sort parameters
|
|
||||||
const validSortFields: DocumentSortField[] = [
|
const validSortFields: DocumentSortField[] = [
|
||||||
'filename',
|
'filename',
|
||||||
'fileSize',
|
'fileSize',
|
||||||
@@ -105,6 +116,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
'chunkCount',
|
'chunkCount',
|
||||||
'uploadedAt',
|
'uploadedAt',
|
||||||
'processingStatus',
|
'processingStatus',
|
||||||
|
'enabled',
|
||||||
]
|
]
|
||||||
const validSortOrders: SortOrder[] = ['asc', 'desc']
|
const validSortOrders: SortOrder[] = ['asc', 'desc']
|
||||||
|
|
||||||
@@ -120,7 +132,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
const result = await getDocuments(
|
const result = await getDocuments(
|
||||||
knowledgeBaseId,
|
knowledgeBaseId,
|
||||||
{
|
{
|
||||||
includeDisabled,
|
enabledFilter: enabledFilter || undefined,
|
||||||
search,
|
search,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
@@ -190,8 +202,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
const createdDocuments = await createDocumentRecords(
|
const createdDocuments = await createDocumentRecords(
|
||||||
validatedData.documents,
|
validatedData.documents,
|
||||||
knowledgeBaseId,
|
knowledgeBaseId,
|
||||||
requestId,
|
requestId
|
||||||
userId
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -250,16 +261,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
throw validationError
|
throw validationError
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle single document creation
|
|
||||||
try {
|
try {
|
||||||
const validatedData = CreateDocumentSchema.parse(body)
|
const validatedData = CreateDocumentSchema.parse(body)
|
||||||
|
|
||||||
const newDocument = await createSingleDocument(
|
const newDocument = await createSingleDocument(validatedData, knowledgeBaseId, requestId)
|
||||||
validatedData,
|
|
||||||
knowledgeBaseId,
|
|
||||||
requestId,
|
|
||||||
userId
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { PlatformEvents } = await import('@/lib/core/telemetry')
|
const { PlatformEvents } = await import('@/lib/core/telemetry')
|
||||||
@@ -294,7 +299,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error creating document`, error)
|
logger.error(`[${requestId}] Error creating document`, error)
|
||||||
|
|
||||||
// Check if it's a storage limit error
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Failed to create document'
|
const errorMessage = error instanceof Error ? error.message : 'Failed to create document'
|
||||||
const isStorageLimitError =
|
const isStorageLimitError =
|
||||||
errorMessage.includes('Storage limit exceeded') || errorMessage.includes('storage limit')
|
errorMessage.includes('Storage limit exceeded') || errorMessage.includes('storage limit')
|
||||||
@@ -331,16 +335,22 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const validatedData = BulkUpdateDocumentsSchema.parse(body)
|
const validatedData = BulkUpdateDocumentsSchema.parse(body)
|
||||||
const { operation, documentIds } = validatedData
|
const { operation, documentIds, selectAll, enabledFilter } = validatedData
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await bulkDocumentOperation(
|
let result
|
||||||
knowledgeBaseId,
|
if (selectAll) {
|
||||||
operation,
|
result = await bulkDocumentOperationByFilter(
|
||||||
documentIds,
|
knowledgeBaseId,
|
||||||
requestId,
|
operation,
|
||||||
session.user.id
|
enabledFilter,
|
||||||
)
|
requestId
|
||||||
|
)
|
||||||
|
} else if (documentIds && documentIds.length > 0) {
|
||||||
|
result = await bulkDocumentOperation(knowledgeBaseId, operation, documentIds, requestId)
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ error: 'No documents specified' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
|
||||||
import { McpClient } from '@/lib/mcp/client'
|
import { McpClient } from '@/lib/mcp/client'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
|
import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config'
|
||||||
|
import type { McpTransport } from '@/lib/mcp/types'
|
||||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||||
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
|
|
||||||
|
|
||||||
const logger = createLogger('McpServerTestAPI')
|
const logger = createLogger('McpServerTestAPI')
|
||||||
|
|
||||||
@@ -19,30 +18,6 @@ function isUrlBasedTransport(transport: McpTransport): boolean {
|
|||||||
return transport === 'streamable-http'
|
return transport === 'streamable-http'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve environment variables in strings
|
|
||||||
*/
|
|
||||||
function resolveEnvVars(value: string, envVars: Record<string, string>): string {
|
|
||||||
const missingVars: string[] = []
|
|
||||||
const resolvedValue = resolveEnvVarReferences(value, envVars, {
|
|
||||||
allowEmbedded: true,
|
|
||||||
resolveExactMatch: true,
|
|
||||||
trimKeys: true,
|
|
||||||
onMissing: 'keep',
|
|
||||||
deep: false,
|
|
||||||
missingKeys: missingVars,
|
|
||||||
}) as string
|
|
||||||
|
|
||||||
if (missingVars.length > 0) {
|
|
||||||
const uniqueMissing = Array.from(new Set(missingVars))
|
|
||||||
uniqueMissing.forEach((envKey) => {
|
|
||||||
logger.warn(`Environment variable "${envKey}" not found in MCP server test`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestConnectionRequest {
|
interface TestConnectionRequest {
|
||||||
name: string
|
name: string
|
||||||
transport: McpTransport
|
transport: McpTransport
|
||||||
@@ -96,39 +71,30 @@ export const POST = withMcpAuth('write')(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolvedUrl = body.url
|
// Build initial config for resolution
|
||||||
let resolvedHeaders = body.headers || {}
|
const initialConfig = {
|
||||||
|
|
||||||
try {
|
|
||||||
const envVars = await getEffectiveDecryptedEnv(userId, workspaceId)
|
|
||||||
|
|
||||||
if (resolvedUrl) {
|
|
||||||
resolvedUrl = resolveEnvVars(resolvedUrl, envVars)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedHeadersObj: Record<string, string> = {}
|
|
||||||
for (const [key, value] of Object.entries(resolvedHeaders)) {
|
|
||||||
resolvedHeadersObj[key] = resolveEnvVars(value, envVars)
|
|
||||||
}
|
|
||||||
resolvedHeaders = resolvedHeadersObj
|
|
||||||
} catch (envError) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Failed to resolve environment variables, using raw values:`,
|
|
||||||
envError
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const testConfig: McpServerConfig = {
|
|
||||||
id: `test-${requestId}`,
|
id: `test-${requestId}`,
|
||||||
name: body.name,
|
name: body.name,
|
||||||
transport: body.transport,
|
transport: body.transport,
|
||||||
url: resolvedUrl,
|
url: body.url,
|
||||||
headers: resolvedHeaders,
|
headers: body.headers || {},
|
||||||
timeout: body.timeout || 10000,
|
timeout: body.timeout || 10000,
|
||||||
retries: 1, // Only one retry for tests
|
retries: 1, // Only one retry for tests
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve env vars using shared utility (non-strict mode for testing)
|
||||||
|
const { config: testConfig, missingVars } = await resolveMcpConfigEnvVars(
|
||||||
|
initialConfig,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
{ strict: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (missingVars.length > 0) {
|
||||||
|
logger.warn(`[${requestId}] Some environment variables not found:`, { missingVars })
|
||||||
|
}
|
||||||
|
|
||||||
const testSecurityPolicy = {
|
const testSecurityPolicy = {
|
||||||
requireConsent: false,
|
requireConsent: false,
|
||||||
auditLevel: 'none' as const,
|
auditLevel: 'none' as const,
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { account } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
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 { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
import type { StreamingExecution } from '@/executor/types'
|
import type { StreamingExecution } from '@/executor/types'
|
||||||
import { executeProviderRequest } from '@/providers'
|
import { executeProviderRequest } from '@/providers'
|
||||||
@@ -20,6 +22,11 @@ export async function POST(request: NextRequest) {
|
|||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[${requestId}] Provider API request started`, {
|
logger.info(`[${requestId}] Provider API request started`, {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
userAgent: request.headers.get('User-Agent'),
|
userAgent: request.headers.get('User-Agent'),
|
||||||
@@ -85,6 +92,13 @@ export async function POST(request: NextRequest) {
|
|||||||
verbosity,
|
verbosity,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (workspaceId) {
|
||||||
|
const workspaceAccess = await checkWorkspaceAccess(workspaceId, auth.userId)
|
||||||
|
if (!workspaceAccess.hasAccess) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let finalApiKey: string | undefined = apiKey
|
let finalApiKey: string | undefined = apiKey
|
||||||
try {
|
try {
|
||||||
if (provider === 'vertex' && vertexCredential) {
|
if (provider === 'vertex' && vertexCredential) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateExternalUrl } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -39,6 +40,18 @@ export async function POST(request: NextRequest) {
|
|||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const validatedData = A2ASetPushNotificationSchema.parse(body)
|
const validatedData = A2ASetPushNotificationSchema.parse(body)
|
||||||
|
|
||||||
|
const urlValidation = validateExternalUrl(validatedData.webhookUrl, 'Webhook URL')
|
||||||
|
if (!urlValidation.isValid) {
|
||||||
|
logger.warn(`[${requestId}] Invalid webhook URL`, { error: urlValidation.error })
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: urlValidation.error,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[${requestId}] A2A set push notification request`, {
|
logger.info(`[${requestId}] A2A set push notification request`, {
|
||||||
agentUrl: validatedData.agentUrl,
|
agentUrl: validatedData.agentUrl,
|
||||||
taskId: validatedData.taskId,
|
taskId: validatedData.taskId,
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'user-123',
|
userId: 'user-123',
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
@@ -254,7 +254,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unauthorized',
|
error: 'Unauthorized',
|
||||||
}),
|
}),
|
||||||
@@ -304,7 +304,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
describe('POST /api/tools/custom', () => {
|
describe('POST /api/tools/custom', () => {
|
||||||
it('should reject unauthorized requests', async () => {
|
it('should reject unauthorized requests', async () => {
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unauthorized',
|
error: 'Unauthorized',
|
||||||
}),
|
}),
|
||||||
@@ -390,7 +390,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
|
|
||||||
it('should prevent unauthorized deletion of user-scoped tool', async () => {
|
it('should prevent unauthorized deletion of user-scoped tool', async () => {
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'user-456',
|
userId: 'user-456',
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
@@ -413,7 +413,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
|
|
||||||
it('should reject unauthorized requests', async () => {
|
it('should reject unauthorized requests', async () => {
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unauthorized',
|
error: 'Unauthorized',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, desc, eq, isNull, or } from 'drizzle-orm'
|
import { and, desc, eq, isNull, or } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -42,8 +42,8 @@ export async function GET(request: NextRequest) {
|
|||||||
const workflowId = searchParams.get('workflowId')
|
const workflowId = searchParams.get('workflowId')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use hybrid auth to support session, API key, and internal JWT
|
// Use session/internal auth to support session and internal JWT (no API key access)
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized custom tools access attempt`)
|
logger.warn(`[${requestId}] Unauthorized custom tools access attempt`)
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
@@ -69,8 +69,8 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check workspace permissions
|
// Check workspace permissions
|
||||||
// For internal JWT with workflowId: checkHybridAuth already resolved userId from workflow owner
|
// For internal JWT with workflowId: checkSessionOrInternalAuth already resolved userId from workflow owner
|
||||||
// For session/API key: verify user has access to the workspace
|
// For session: verify user has access to the workspace
|
||||||
// For legacy (no workspaceId): skip workspace check, rely on userId match
|
// For legacy (no workspaceId): skip workspace check, rely on userId match
|
||||||
if (resolvedWorkspaceId && !(authResult.authType === 'internal_jwt' && workflowId)) {
|
if (resolvedWorkspaceId && !(authResult.authType === 'internal_jwt' && workflowId)) {
|
||||||
const userPermission = await getUserEntityPermissions(
|
const userPermission = await getUserEntityPermissions(
|
||||||
@@ -116,8 +116,8 @@ export async function POST(req: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use hybrid auth (though this endpoint is only called from UI)
|
// Use session/internal auth (no API key access)
|
||||||
const authResult = await checkHybridAuth(req, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized custom tools update attempt`)
|
logger.warn(`[${requestId}] Unauthorized custom tools update attempt`)
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
@@ -193,8 +193,8 @@ export async function DELETE(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use hybrid auth (though this endpoint is only called from UI)
|
// Use session/internal auth (no API key access)
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized custom tool deletion attempt`)
|
logger.warn(`[${requestId}] Unauthorized custom tool deletion attempt`)
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateNumericId } from '@/lib/core/security/input-validation'
|
import { validateNumericId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail add label attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail add label attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail archive attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail archive attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail delete attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail delete attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail mark read attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail mark read attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail mark unread attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail mark unread attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail move attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail move attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail remove label attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail remove label attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail unarchive attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail unarchive attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateImageUrl } from '@/lib/core/security/input-validation'
|
import { validateImageUrl } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const imageUrl = url.searchParams.get('url')
|
const imageUrl = url.searchParams.get('url')
|
||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.error(`[${requestId}] Authentication failed for image proxy:`, authResult.error)
|
logger.error(`[${requestId}] Authentication failed for image proxy:`, authResult.error)
|
||||||
return new NextResponse('Unauthorized', { status: 401 })
|
return new NextResponse('Unauthorized', { status: 401 })
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { Resend } from 'resend'
|
import { Resend } from 'resend'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized mail send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized mail send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Teams chat delete attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Teams chat delete attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Teams channel write attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Teams channel write attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Teams chat write attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Teams chat write attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { StorageService } from '@/lib/uploads'
|
import { StorageService } from '@/lib/uploads'
|
||||||
@@ -30,7 +30,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Mistral parse attempt`, {
|
logger.warn(`[${requestId}] Unauthorized Mistral parse attempt`, {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLDeleteAPI')
|
const logger = createLogger('MySQLDeleteAPI')
|
||||||
@@ -21,6 +22,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized MySQL delete attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = DeleteSchema.parse(body)
|
const params = DeleteSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLExecuteAPI')
|
const logger = createLogger('MySQLExecuteAPI')
|
||||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized MySQL execute attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = ExecuteSchema.parse(body)
|
const params = ExecuteSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLInsertAPI')
|
const logger = createLogger('MySQLInsertAPI')
|
||||||
@@ -42,6 +43,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized MySQL insert attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = InsertSchema.parse(body)
|
const params = InsertSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils'
|
import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLIntrospectAPI')
|
const logger = createLogger('MySQLIntrospectAPI')
|
||||||
@@ -19,6 +20,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized MySQL introspect attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = IntrospectSchema.parse(body)
|
const params = IntrospectSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLQueryAPI')
|
const logger = createLogger('MySQLQueryAPI')
|
||||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized MySQL query attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = QuerySchema.parse(body)
|
const params = QuerySchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { buildUpdateQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
import { buildUpdateQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLUpdateAPI')
|
const logger = createLogger('MySQLUpdateAPI')
|
||||||
@@ -40,6 +41,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized MySQL update attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = UpdateSchema.parse(body)
|
const params = UpdateSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import * as XLSX from 'xlsx'
|
import * as XLSX from 'xlsx'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import {
|
import {
|
||||||
@@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook copy attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook copy attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook delete attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook delete attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook mark read attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook mark read attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook mark unread attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook mark unread attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook move attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook move attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { createPostgresConnection, executeDelete } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeDelete } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLDeleteAPI')
|
const logger = createLogger('PostgreSQLDeleteAPI')
|
||||||
@@ -21,6 +22,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized PostgreSQL delete attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = DeleteSchema.parse(body)
|
const params = DeleteSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import {
|
import {
|
||||||
createPostgresConnection,
|
createPostgresConnection,
|
||||||
executeQuery,
|
executeQuery,
|
||||||
@@ -24,6 +25,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized PostgreSQL execute attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = ExecuteSchema.parse(body)
|
const params = ExecuteSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { createPostgresConnection, executeInsert } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeInsert } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLInsertAPI')
|
const logger = createLogger('PostgreSQLInsertAPI')
|
||||||
@@ -42,6 +43,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized PostgreSQL insert attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
|
|
||||||
const params = InsertSchema.parse(body)
|
const params = InsertSchema.parse(body)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { createPostgresConnection, executeIntrospect } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeIntrospect } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLIntrospectAPI')
|
const logger = createLogger('PostgreSQLIntrospectAPI')
|
||||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized PostgreSQL introspect attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = IntrospectSchema.parse(body)
|
const params = IntrospectSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { createPostgresConnection, executeQuery } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeQuery } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLQueryAPI')
|
const logger = createLogger('PostgreSQLQueryAPI')
|
||||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized PostgreSQL query attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = QuerySchema.parse(body)
|
const params = QuerySchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { createPostgresConnection, executeUpdate } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeUpdate } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLUpdateAPI')
|
const logger = createLogger('PostgreSQLUpdateAPI')
|
||||||
@@ -40,6 +41,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized PostgreSQL update attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = UpdateSchema.parse(body)
|
const params = UpdateSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { StorageService } from '@/lib/uploads'
|
import { StorageService } from '@/lib/uploads'
|
||||||
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Pulse parse attempt`, {
|
logger.warn(`[${requestId}] Unauthorized Pulse parse attempt`, {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { StorageService } from '@/lib/uploads'
|
import { StorageService } from '@/lib/uploads'
|
||||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Reducto parse attempt`, {
|
logger.warn(`[${requestId}] Unauthorized Reducto parse attempt`, {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CopyObjectCommand, type ObjectCannedACL, S3Client } from '@aws-sdk/clie
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized S3 copy object attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized S3 copy object attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized S3 delete object attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized S3 delete object attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized S3 list objects attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized S3 list objects attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type ObjectCannedACL, PutObjectCommand, S3Client } from '@aws-sdk/clien
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { SEARCH_TOOL_COST } from '@/lib/billing/constants'
|
import { SEARCH_TOOL_COST } from '@/lib/billing/constants'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { executeTool } from '@/tools'
|
import { executeTool } from '@/tools'
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const { searchParams: urlParams } = new URL(request.url)
|
const { searchParams: urlParams } = new URL(request.url)
|
||||||
const workflowId = urlParams.get('workflowId') || undefined
|
const workflowId = urlParams.get('workflowId') || undefined
|
||||||
|
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
const errorMessage = workflowId ? 'Workflow not found' : authResult.error || 'Unauthorized'
|
const errorMessage = workflowId ? 'Workflow not found' : authResult.error || 'Unauthorized'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import type { SFTPWrapper } from 'ssh2'
|
import type { SFTPWrapper } from 'ssh2'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import {
|
import {
|
||||||
createSftpConnection,
|
createSftpConnection,
|
||||||
@@ -72,7 +72,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SFTP delete attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SFTP delete attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from 'path'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { createSftpConnection, getSftp, isPathSafe, sanitizePath } from '@/app/api/tools/sftp/utils'
|
import { createSftpConnection, getSftp, isPathSafe, sanitizePath } from '@/app/api/tools/sftp/utils'
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SFTP download attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SFTP download attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import {
|
import {
|
||||||
createSftpConnection,
|
createSftpConnection,
|
||||||
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SFTP list attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SFTP list attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import type { SFTPWrapper } from 'ssh2'
|
import type { SFTPWrapper } from 'ssh2'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import {
|
import {
|
||||||
createSftpConnection,
|
createSftpConnection,
|
||||||
@@ -60,7 +60,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SFTP mkdir attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SFTP mkdir attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -44,7 +44,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SFTP upload attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SFTP upload attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SharePoint upload attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SharePoint upload attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ const SlackAddReactionSchema = z.object({
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ const SlackDeleteMessageSchema = z.object({
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { openDMChannel } from '../utils'
|
import { openDMChannel } from '../utils'
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Slack read messages attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Slack read messages attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { sendSlackMessage } from '../utils'
|
import { sendSlackMessage } from '../utils'
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Slack send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Slack send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Slack update message attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Slack update message attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { type SMSOptions, sendSMS } from '@/lib/messaging/sms/service'
|
import { type SMSOptions, sendSMS } from '@/lib/messaging/sms/service'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SMS send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SMS send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SMTP send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SMTP send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { createSSHConnection, escapeShellArg, executeSSHCommand } from '@/app/api/tools/ssh/utils'
|
import { createSSHConnection, escapeShellArg, executeSSHCommand } from '@/app/api/tools/ssh/utils'
|
||||||
|
|
||||||
const logger = createLogger('SSHCheckCommandExistsAPI')
|
const logger = createLogger('SSHCheckCommandExistsAPI')
|
||||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized SSH check command exists attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = CheckCommandExistsSchema.parse(body)
|
const params = CheckCommandExistsSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import type { Client, SFTPWrapper, Stats } from 'ssh2'
|
import type { Client, SFTPWrapper, Stats } from 'ssh2'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import {
|
import {
|
||||||
createSSHConnection,
|
createSSHConnection,
|
||||||
getFileType,
|
getFileType,
|
||||||
@@ -39,10 +40,15 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized SSH check file exists attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = CheckFileExistsSchema.parse(body)
|
const params = CheckFileExistsSchema.parse(body)
|
||||||
|
|
||||||
// Validate authentication
|
|
||||||
if (!params.password && !params.privateKey) {
|
if (!params.password && !params.privateKey) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Either password or privateKey must be provided' },
|
{ error: 'Either password or privateKey must be provided' },
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import {
|
import {
|
||||||
createSSHConnection,
|
createSSHConnection,
|
||||||
escapeShellArg,
|
escapeShellArg,
|
||||||
@@ -27,10 +28,15 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized SSH create directory attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = CreateDirectorySchema.parse(body)
|
const params = CreateDirectorySchema.parse(body)
|
||||||
|
|
||||||
// Validate authentication
|
|
||||||
if (!params.password && !params.privateKey) {
|
if (!params.password && !params.privateKey) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Either password or privateKey must be provided' },
|
{ error: 'Either password or privateKey must be provided' },
|
||||||
@@ -53,7 +59,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const dirPath = sanitizePath(params.path)
|
const dirPath = sanitizePath(params.path)
|
||||||
const escapedPath = escapeShellArg(dirPath)
|
const escapedPath = escapeShellArg(dirPath)
|
||||||
|
|
||||||
// Check if directory already exists
|
|
||||||
const checkResult = await executeSSHCommand(
|
const checkResult = await executeSSHCommand(
|
||||||
client,
|
client,
|
||||||
`test -d '${escapedPath}' && echo "exists"`
|
`test -d '${escapedPath}' && echo "exists"`
|
||||||
@@ -70,7 +75,6 @@ export async function POST(request: NextRequest) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create directory
|
|
||||||
const mkdirFlag = params.recursive ? '-p' : ''
|
const mkdirFlag = params.recursive ? '-p' : ''
|
||||||
const command = `mkdir ${mkdirFlag} -m ${params.permissions} '${escapedPath}'`
|
const command = `mkdir ${mkdirFlag} -m ${params.permissions} '${escapedPath}'`
|
||||||
const result = await executeSSHCommand(client, command)
|
const result = await executeSSHCommand(client, command)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import {
|
import {
|
||||||
createSSHConnection,
|
createSSHConnection,
|
||||||
escapeShellArg,
|
escapeShellArg,
|
||||||
@@ -27,10 +28,15 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const auth = await checkInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized SSH delete file attempt`)
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = DeleteFileSchema.parse(body)
|
const params = DeleteFileSchema.parse(body)
|
||||||
|
|
||||||
// Validate authentication
|
|
||||||
if (!params.password && !params.privateKey) {
|
if (!params.password && !params.privateKey) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Either password or privateKey must be provided' },
|
{ error: 'Either password or privateKey must be provided' },
|
||||||
@@ -53,7 +59,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const filePath = sanitizePath(params.path)
|
const filePath = sanitizePath(params.path)
|
||||||
const escapedPath = escapeShellArg(filePath)
|
const escapedPath = escapeShellArg(filePath)
|
||||||
|
|
||||||
// Check if path exists
|
|
||||||
const checkResult = await executeSSHCommand(
|
const checkResult = await executeSSHCommand(
|
||||||
client,
|
client,
|
||||||
`test -e '${escapedPath}' && echo "exists"`
|
`test -e '${escapedPath}' && echo "exists"`
|
||||||
@@ -62,7 +67,6 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: `Path does not exist: ${filePath}` }, { status: 404 })
|
return NextResponse.json({ error: `Path does not exist: ${filePath}` }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build delete command
|
|
||||||
let command: string
|
let command: string
|
||||||
if (params.recursive) {
|
if (params.recursive) {
|
||||||
command = params.force ? `rm -rf '${escapedPath}'` : `rm -r '${escapedPath}'`
|
command = params.force ? `rm -rf '${escapedPath}'` : `rm -r '${escapedPath}'`
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user