mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-26 15:28:03 -05:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ecd07b12c | ||
|
|
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 |
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]}"
|
||||||
|
|||||||
@@ -4696,26 +4696,6 @@ export function BedrockIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
viewBox='0 0 24 24'
|
|
||||||
fill='none'
|
|
||||||
stroke='currentColor'
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeLinecap='round'
|
|
||||||
strokeLinejoin='round'
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<rect width='18' height='18' x='3' y='3' rx='2' />
|
|
||||||
<path d='M3 9h18' />
|
|
||||||
<path d='M3 15h18' />
|
|
||||||
<path d='M9 3v18' />
|
|
||||||
<path d='M15 3v18' />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export function ReductoIcon(props: SVGProps<SVGSVGElement>) {
|
export function ReductoIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ import {
|
|||||||
StagehandIcon,
|
StagehandIcon,
|
||||||
StripeIcon,
|
StripeIcon,
|
||||||
SupabaseIcon,
|
SupabaseIcon,
|
||||||
TableIcon,
|
|
||||||
TavilyIcon,
|
TavilyIcon,
|
||||||
TelegramIcon,
|
TelegramIcon,
|
||||||
TextractIcon,
|
TextractIcon,
|
||||||
@@ -237,7 +236,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
|||||||
stripe: StripeIcon,
|
stripe: StripeIcon,
|
||||||
stt: STTIcon,
|
stt: STTIcon,
|
||||||
supabase: SupabaseIcon,
|
supabase: SupabaseIcon,
|
||||||
table: TableIcon,
|
|
||||||
tavily: TavilyIcon,
|
tavily: TavilyIcon,
|
||||||
telegram: TelegramIcon,
|
telegram: TelegramIcon,
|
||||||
textract: TextractIcon,
|
textract: TextractIcon,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -104,7 +104,6 @@
|
|||||||
"stripe",
|
"stripe",
|
||||||
"stt",
|
"stt",
|
||||||
"supabase",
|
"supabase",
|
||||||
"table",
|
|
||||||
"tavily",
|
"tavily",
|
||||||
"telegram",
|
"telegram",
|
||||||
"textract",
|
"textract",
|
||||||
|
|||||||
@@ -1,351 +0,0 @@
|
|||||||
---
|
|
||||||
title: Table
|
|
||||||
description: User-defined data tables for storing and querying structured data
|
|
||||||
---
|
|
||||||
|
|
||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|
||||||
|
|
||||||
<BlockInfoCard
|
|
||||||
type="table"
|
|
||||||
color="#10B981"
|
|
||||||
/>
|
|
||||||
|
|
||||||
Tables allow you to create and manage custom data tables directly within Sim. Store, query, and manipulate structured data within your workflows without needing external database integrations.
|
|
||||||
|
|
||||||
**Why Use Tables?**
|
|
||||||
- **No external setup**: Create tables instantly without configuring external databases
|
|
||||||
- **Workflow-native**: Data persists across workflow executions and is accessible from any workflow in your workspace
|
|
||||||
- **Flexible schema**: Define columns with types (string, number, boolean, date, json) and constraints (required, unique)
|
|
||||||
- **Powerful querying**: Filter, sort, and paginate data using MongoDB-style operators
|
|
||||||
- **Agent-friendly**: Tables can be used as tools by AI agents for dynamic data storage and retrieval
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- Create tables with custom schemas
|
|
||||||
- Insert, update, upsert, and delete rows
|
|
||||||
- Query with filters and sorting
|
|
||||||
- Batch operations for bulk inserts
|
|
||||||
- Bulk updates and deletes by filter
|
|
||||||
- Up to 10,000 rows per table, 100 tables per workspace
|
|
||||||
|
|
||||||
## Creating Tables
|
|
||||||
|
|
||||||
Tables are created from the **Tables** section in the sidebar. Each table requires:
|
|
||||||
- **Name**: Alphanumeric with underscores (e.g., `customer_leads`)
|
|
||||||
- **Description**: Optional description of the table's purpose
|
|
||||||
- **Schema**: Define columns with name, type, and optional constraints
|
|
||||||
|
|
||||||
### Column Types
|
|
||||||
|
|
||||||
| Type | Description | Example Values |
|
|
||||||
|------|-------------|----------------|
|
|
||||||
| `string` | Text data | `"John Doe"`, `"active"` |
|
|
||||||
| `number` | Numeric data | `42`, `99.99` |
|
|
||||||
| `boolean` | True/false values | `true`, `false` |
|
|
||||||
| `date` | Date/time values | `"2024-01-15T10:30:00Z"` |
|
|
||||||
| `json` | Complex nested data | `{"address": {"city": "NYC"}}` |
|
|
||||||
|
|
||||||
### Column Constraints
|
|
||||||
|
|
||||||
- **Required**: Column must have a value (cannot be null)
|
|
||||||
- **Unique**: Values must be unique across all rows (enables upsert matching)
|
|
||||||
|
|
||||||
## Usage Instructions
|
|
||||||
|
|
||||||
Create and manage custom data tables. Store, query, and manipulate structured data within workflows.
|
|
||||||
|
|
||||||
## Tools
|
|
||||||
|
|
||||||
### `table_query_rows`
|
|
||||||
|
|
||||||
Query rows from a table with filtering, sorting, and pagination
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
| `filter` | object | No | Filter conditions using MongoDB-style operators |
|
|
||||||
| `sort` | object | No | Sort order as \{column: "asc"\|"desc"\} |
|
|
||||||
| `limit` | number | No | Maximum rows to return \(default: 100, max: 1000\) |
|
|
||||||
| `offset` | number | No | Number of rows to skip \(default: 0\) |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether query succeeded |
|
|
||||||
| `rows` | array | Query result rows |
|
|
||||||
| `rowCount` | number | Number of rows returned |
|
|
||||||
| `totalCount` | number | Total rows matching filter |
|
|
||||||
| `limit` | number | Limit used in query |
|
|
||||||
| `offset` | number | Offset used in query |
|
|
||||||
|
|
||||||
### `table_insert_row`
|
|
||||||
|
|
||||||
Insert a new row into a table
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
| `data` | object | Yes | Row data as JSON object matching the table schema |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether row was inserted |
|
|
||||||
| `row` | object | Inserted row data including generated ID |
|
|
||||||
| `message` | string | Status message |
|
|
||||||
|
|
||||||
### `table_upsert_row`
|
|
||||||
|
|
||||||
Insert or update a row based on unique column constraints. If a row with matching unique field exists, update it; otherwise insert a new row.
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
| `data` | object | Yes | Row data to insert or update |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether row was upserted |
|
|
||||||
| `row` | object | Upserted row data |
|
|
||||||
| `operation` | string | Operation performed: "insert" or "update" |
|
|
||||||
| `message` | string | Status message |
|
|
||||||
|
|
||||||
### `table_batch_insert_rows`
|
|
||||||
|
|
||||||
Insert multiple rows at once (up to 1000 rows per batch)
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
| `rows` | array | Yes | Array of row data objects to insert |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether batch insert succeeded |
|
|
||||||
| `rows` | array | Array of inserted rows with IDs |
|
|
||||||
| `insertedCount` | number | Number of rows inserted |
|
|
||||||
| `message` | string | Status message |
|
|
||||||
|
|
||||||
### `table_update_row`
|
|
||||||
|
|
||||||
Update a specific row by its ID
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
| `rowId` | string | Yes | Row ID to update |
|
|
||||||
| `data` | object | Yes | Data to update \(partial update supported\) |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether row was updated |
|
|
||||||
| `row` | object | Updated row data |
|
|
||||||
| `message` | string | Status message |
|
|
||||||
|
|
||||||
### `table_update_rows_by_filter`
|
|
||||||
|
|
||||||
Update multiple rows matching a filter condition
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
| `filter` | object | Yes | Filter to match rows for update |
|
|
||||||
| `data` | object | Yes | Data to apply to matching rows |
|
|
||||||
| `limit` | number | No | Maximum rows to update \(default: 1000\) |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether update succeeded |
|
|
||||||
| `updatedCount` | number | Number of rows updated |
|
|
||||||
| `updatedRowIds` | array | IDs of updated rows |
|
|
||||||
| `message` | string | Status message |
|
|
||||||
|
|
||||||
### `table_delete_row`
|
|
||||||
|
|
||||||
Delete a specific row by its ID
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
| `rowId` | string | Yes | Row ID to delete |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether row was deleted |
|
|
||||||
| `deletedCount` | number | Number of rows deleted \(1 or 0\) |
|
|
||||||
| `message` | string | Status message |
|
|
||||||
|
|
||||||
### `table_delete_rows_by_filter`
|
|
||||||
|
|
||||||
Delete multiple rows matching a filter condition
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
| `filter` | object | Yes | Filter to match rows for deletion |
|
|
||||||
| `limit` | number | No | Maximum rows to delete \(default: 1000\) |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether delete succeeded |
|
|
||||||
| `deletedCount` | number | Number of rows deleted |
|
|
||||||
| `deletedRowIds` | array | IDs of deleted rows |
|
|
||||||
| `message` | string | Status message |
|
|
||||||
|
|
||||||
### `table_get_row`
|
|
||||||
|
|
||||||
Get a single row by its ID
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
| `rowId` | string | Yes | Row ID to retrieve |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether row was found |
|
|
||||||
| `row` | object | Row data |
|
|
||||||
| `message` | string | Status message |
|
|
||||||
|
|
||||||
### `table_get_schema`
|
|
||||||
|
|
||||||
Get the schema definition for a table
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `tableId` | string | Yes | Table ID |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `success` | boolean | Whether schema was retrieved |
|
|
||||||
| `name` | string | Table name |
|
|
||||||
| `columns` | array | Array of column definitions |
|
|
||||||
| `message` | string | Status message |
|
|
||||||
|
|
||||||
## Filter Operators
|
|
||||||
|
|
||||||
Filters use MongoDB-style operators for flexible querying:
|
|
||||||
|
|
||||||
| Operator | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `$eq` | Equals | `{"status": {"$eq": "active"}}` or `{"status": "active"}` |
|
|
||||||
| `$ne` | Not equals | `{"status": {"$ne": "deleted"}}` |
|
|
||||||
| `$gt` | Greater than | `{"age": {"$gt": 18}}` |
|
|
||||||
| `$gte` | Greater than or equal | `{"score": {"$gte": 80}}` |
|
|
||||||
| `$lt` | Less than | `{"price": {"$lt": 100}}` |
|
|
||||||
| `$lte` | Less than or equal | `{"quantity": {"$lte": 10}}` |
|
|
||||||
| `$in` | In array | `{"status": {"$in": ["active", "pending"]}}` |
|
|
||||||
| `$nin` | Not in array | `{"type": {"$nin": ["spam", "blocked"]}}` |
|
|
||||||
| `$contains` | String contains | `{"email": {"$contains": "@gmail.com"}}` |
|
|
||||||
|
|
||||||
### Combining Filters
|
|
||||||
|
|
||||||
Multiple field conditions are combined with AND logic:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "active",
|
|
||||||
"age": {"$gte": 18}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `$or` for OR logic:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"$or": [
|
|
||||||
{"status": "active"},
|
|
||||||
{"status": "pending"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sort Specification
|
|
||||||
|
|
||||||
Specify sort order with column names and direction:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"createdAt": "desc"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Multi-column sorting:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"priority": "desc",
|
|
||||||
"name": "asc"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Built-in Columns
|
|
||||||
|
|
||||||
Every row automatically includes:
|
|
||||||
|
|
||||||
| Column | Type | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| `id` | string | Unique row identifier |
|
|
||||||
| `createdAt` | date | When the row was created |
|
|
||||||
| `updatedAt` | date | When the row was last modified |
|
|
||||||
|
|
||||||
These can be used in filters and sorting.
|
|
||||||
|
|
||||||
## Limits
|
|
||||||
|
|
||||||
| Resource | Limit |
|
|
||||||
|----------|-------|
|
|
||||||
| Tables per workspace | 100 |
|
|
||||||
| Rows per table | 10,000 |
|
|
||||||
| Columns per table | 50 |
|
|
||||||
| Max row size | 100KB |
|
|
||||||
| String value length | 10,000 characters |
|
|
||||||
| Query limit | 1,000 rows |
|
|
||||||
| Batch insert size | 1,000 rows |
|
|
||||||
| Bulk update/delete | 1,000 rows |
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Category: `blocks`
|
|
||||||
- Type: `table`
|
|
||||||
- Tables are scoped to workspaces and accessible from any workflow within that workspace
|
|
||||||
- Data persists across workflow executions
|
|
||||||
- Use unique constraints to enable upsert functionality
|
|
||||||
- The visual filter/sort builder provides an easy way to construct queries without writing JSON
|
|
||||||
@@ -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,6 +21,7 @@ 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'
|
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||||
@@ -107,7 +107,6 @@ export default function LoginPage({
|
|||||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||||
const [showValidationError, setShowValidationError] = useState(false)
|
const [showValidationError, setShowValidationError] = useState(false)
|
||||||
const buttonClass = useBrandedButtonClass()
|
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)
|
||||||
@@ -115,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
|
||||||
@@ -184,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()
|
||||||
@@ -215,9 +220,9 @@ export default function LoginPage({
|
|||||||
onError: (ctx) => {
|
onError: (ctx) => {
|
||||||
logger.error('Login error:', ctx.error)
|
logger.error('Login error:', ctx.error)
|
||||||
|
|
||||||
// EMAIL_NOT_VERIFIED is handled by the catch block which redirects to /verify
|
|
||||||
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
|
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
|
||||||
errorHandled = true
|
errorHandled = true
|
||||||
|
redirectToVerify(email)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,10 +290,7 @@ export default function LoginPage({
|
|||||||
router.push(safeCallbackUrl)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -619,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,13 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { 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 { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||||
|
|
||||||
interface RequestResetFormProps {
|
interface RequestResetFormProps {
|
||||||
email: string
|
email: string
|
||||||
@@ -28,9 +27,6 @@ export function RequestResetForm({
|
|||||||
statusMessage,
|
statusMessage,
|
||||||
className,
|
className,
|
||||||
}: RequestResetFormProps) {
|
}: RequestResetFormProps) {
|
||||||
const buttonClass = useBrandedButtonClass()
|
|
||||||
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onSubmit(email)
|
onSubmit(email)
|
||||||
@@ -68,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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -112,8 +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 = useBrandedButtonClass()
|
|
||||||
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -243,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,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 { 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'
|
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||||
@@ -97,7 +97,6 @@ function SignupFormContent({
|
|||||||
const [redirectUrl, setRedirectUrl] = useState('')
|
const [redirectUrl, setRedirectUrl] = useState('')
|
||||||
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
||||||
const buttonClass = useBrandedButtonClass()
|
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[]>([])
|
||||||
@@ -476,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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"',
|
||||||
@@ -313,7 +339,7 @@ describe('Function Execute API Route', () => {
|
|||||||
'block-2': 'world',
|
'block-2': 'world',
|
||||||
},
|
},
|
||||||
blockNameMapping: {
|
blockNameMapping: {
|
||||||
validVar: 'block-1',
|
validvar: 'block-1',
|
||||||
another_valid: 'block-2',
|
another_valid: 'block-2',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -539,7 +565,7 @@ describe('Function Execute API Route', () => {
|
|||||||
'block-complex': complexData,
|
'block-complex': complexData,
|
||||||
},
|
},
|
||||||
blockNameMapping: {
|
blockNameMapping: {
|
||||||
complexData: 'block-complex',
|
complexdata: 'block-complex',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
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,
|
||||||
} from '@/executor/utils/reference-validation'
|
} from '@/executor/utils/reference-validation'
|
||||||
import { navigatePath } from '@/executor/variables/resolvers/reference'
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
export const runtime = 'nodejs'
|
export const runtime = 'nodejs'
|
||||||
|
|
||||||
@@ -470,14 +471,17 @@ function resolveEnvironmentVariables(
|
|||||||
|
|
||||||
function resolveTagVariables(
|
function resolveTagVariables(
|
||||||
code: string,
|
code: string,
|
||||||
blockData: Record<string, any>,
|
blockData: Record<string, unknown>,
|
||||||
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) || []
|
||||||
@@ -486,41 +490,37 @@ function resolveTagVariables(
|
|||||||
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 pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
|
||||||
const blockName = pathParts[0]
|
const blockName = pathParts[0]
|
||||||
|
const fieldPath = pathParts.slice(1)
|
||||||
|
|
||||||
const blockId = blockNameMapping[blockName]
|
const result = resolveBlockReference(blockName, fieldPath, {
|
||||||
if (!blockId) {
|
blockNameMapping,
|
||||||
|
blockData,
|
||||||
|
blockOutputSchemas,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockOutput = blockData[blockId]
|
let tagValue = result.value
|
||||||
if (blockOutput === undefined) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let tagValue: any
|
|
||||||
if (pathParts.length === 1) {
|
|
||||||
tagValue = blockOutput
|
|
||||||
} else {
|
|
||||||
tagValue = navigatePath(blockOutput, pathParts.slice(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tagValue === undefined) {
|
if (tagValue === undefined) {
|
||||||
|
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), undefinedLiteral)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (typeof tagValue === 'string') {
|
||||||
typeof tagValue === 'string' &&
|
const trimmed = tagValue.trimStart()
|
||||||
tagValue.length > 100 &&
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
(tagValue.startsWith('{') || tagValue.startsWith('['))
|
try {
|
||||||
) {
|
tagValue = JSON.parse(tagValue)
|
||||||
try {
|
} catch {
|
||||||
tagValue = JSON.parse(tagValue)
|
// Keep as string if not valid JSON
|
||||||
} catch {
|
}
|
||||||
// Keep as-is
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
const safeVarName = `__tag_${tagName.replace(/_/g, '_1').replace(/\./g, '_0')}`
|
||||||
contextVariables[safeVarName] = tagValue
|
contextVariables[safeVarName] = tagValue
|
||||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||||
}
|
}
|
||||||
@@ -537,18 +537,27 @@ 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> = {}
|
||||||
|
|
||||||
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
|
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
|
||||||
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
|
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
|
||||||
resolvedCode = resolveTagVariables(resolvedCode, blockData, blockNameMapping, contextVariables)
|
resolvedCode = resolveTagVariables(
|
||||||
|
resolvedCode,
|
||||||
|
blockData,
|
||||||
|
blockNameMapping,
|
||||||
|
blockOutputSchemas,
|
||||||
|
contextVariables,
|
||||||
|
language
|
||||||
|
)
|
||||||
|
|
||||||
return { resolvedCode, contextVariables }
|
return { resolvedCode, contextVariables }
|
||||||
}
|
}
|
||||||
@@ -573,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')
|
||||||
@@ -585,6 +600,7 @@ export async function POST(req: NextRequest) {
|
|||||||
envVars = {},
|
envVars = {},
|
||||||
blockData = {},
|
blockData = {},
|
||||||
blockNameMapping = {},
|
blockNameMapping = {},
|
||||||
|
blockOutputSchemas = {},
|
||||||
workflowVariables = {},
|
workflowVariables = {},
|
||||||
workflowId,
|
workflowId,
|
||||||
isCustomTool = false,
|
isCustomTool = false,
|
||||||
@@ -601,20 +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
|
|
||||||
|
|
||||||
let jsImports = ''
|
let jsImports = ''
|
||||||
let jsRemainingCode = resolvedCode
|
let jsRemainingCode = resolvedCode
|
||||||
let hasImports = false
|
let hasImports = false
|
||||||
@@ -670,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++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,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 = [
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
|
||||||
import { deleteTable, type TableSchema } from '@/lib/table'
|
|
||||||
import { accessError, checkAccess, normalizeColumn, verifyTableWorkspace } from '../utils'
|
|
||||||
|
|
||||||
const logger = createLogger('TableDetailAPI')
|
|
||||||
|
|
||||||
const GetTableSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface TableRouteParams {
|
|
||||||
params: Promise<{ tableId: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET /api/table/[tableId] - Retrieves a single table's details. */
|
|
||||||
export async function GET(request: NextRequest, { params }: TableRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
logger.warn(`[${requestId}] Unauthorized table access attempt`)
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const validated = GetTableSchema.parse({
|
|
||||||
workspaceId: searchParams.get('workspaceId'),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await checkAccess(tableId, authResult.userId, 'read')
|
|
||||||
if (!result.ok) return accessError(result, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = result
|
|
||||||
|
|
||||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
|
||||||
if (!isValidWorkspace) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Retrieved table ${tableId} for user ${authResult.userId}`)
|
|
||||||
|
|
||||||
const schemaData = table.schema as TableSchema
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
table: {
|
|
||||||
id: table.id,
|
|
||||||
name: table.name,
|
|
||||||
description: table.description,
|
|
||||||
schema: {
|
|
||||||
columns: schemaData.columns.map(normalizeColumn),
|
|
||||||
},
|
|
||||||
rowCount: table.rowCount,
|
|
||||||
maxRows: table.maxRows,
|
|
||||||
createdAt:
|
|
||||||
table.createdAt instanceof Date
|
|
||||||
? table.createdAt.toISOString()
|
|
||||||
: String(table.createdAt),
|
|
||||||
updatedAt:
|
|
||||||
table.updatedAt instanceof Date
|
|
||||||
? table.updatedAt.toISOString()
|
|
||||||
: String(table.updatedAt),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error getting table:`, error)
|
|
||||||
return NextResponse.json({ error: 'Failed to get table' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** DELETE /api/table/[tableId] - Deletes a table and all its rows. */
|
|
||||||
export async function DELETE(request: NextRequest, { params }: TableRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
logger.warn(`[${requestId}] Unauthorized table delete attempt`)
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const validated = GetTableSchema.parse({
|
|
||||||
workspaceId: searchParams.get('workspaceId'),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await checkAccess(tableId, authResult.userId, 'write')
|
|
||||||
if (!result.ok) return accessError(result, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = result
|
|
||||||
|
|
||||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
|
||||||
if (!isValidWorkspace) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteTable(tableId, requestId)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
message: 'Table deleted successfully',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error deleting table:`, error)
|
|
||||||
return NextResponse.json({ error: 'Failed to delete table' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
import { db } from '@sim/db'
|
|
||||||
import { userTableRows } from '@sim/db/schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { and, eq } from 'drizzle-orm'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
|
||||||
import type { RowData, TableSchema } from '@/lib/table'
|
|
||||||
import { validateRowData } from '@/lib/table'
|
|
||||||
import { accessError, checkAccess, verifyTableWorkspace } from '../../../utils'
|
|
||||||
|
|
||||||
const logger = createLogger('TableRowAPI')
|
|
||||||
|
|
||||||
const GetRowSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
})
|
|
||||||
|
|
||||||
const UpdateRowSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
data: z.record(z.unknown(), { required_error: 'Row data is required' }),
|
|
||||||
})
|
|
||||||
|
|
||||||
const DeleteRowSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface RowRouteParams {
|
|
||||||
params: Promise<{ tableId: string; rowId: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET /api/table/[tableId]/rows/[rowId] - Retrieves a single row. */
|
|
||||||
export async function GET(request: NextRequest, { params }: RowRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId, rowId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const validated = GetRowSchema.parse({
|
|
||||||
workspaceId: searchParams.get('workspaceId'),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await checkAccess(tableId, authResult.userId, 'read')
|
|
||||||
if (!result.ok) return accessError(result, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = result
|
|
||||||
|
|
||||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
|
||||||
if (!isValidWorkspace) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const [row] = await db
|
|
||||||
.select({
|
|
||||||
id: userTableRows.id,
|
|
||||||
data: userTableRows.data,
|
|
||||||
createdAt: userTableRows.createdAt,
|
|
||||||
updatedAt: userTableRows.updatedAt,
|
|
||||||
})
|
|
||||||
.from(userTableRows)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userTableRows.id, rowId),
|
|
||||||
eq(userTableRows.tableId, tableId),
|
|
||||||
eq(userTableRows.workspaceId, validated.workspaceId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
return NextResponse.json({ error: 'Row not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Retrieved row ${rowId} from table ${tableId}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
row: {
|
|
||||||
id: row.id,
|
|
||||||
data: row.data,
|
|
||||||
createdAt: row.createdAt.toISOString(),
|
|
||||||
updatedAt: row.updatedAt.toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error getting row:`, error)
|
|
||||||
return NextResponse.json({ error: 'Failed to get row' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** PATCH /api/table/[tableId]/rows/[rowId] - Updates a single row (supports partial updates). */
|
|
||||||
export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId, rowId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: unknown = await request.json()
|
|
||||||
const validated = UpdateRowSchema.parse(body)
|
|
||||||
|
|
||||||
const result = await checkAccess(tableId, authResult.userId, 'write')
|
|
||||||
if (!result.ok) return accessError(result, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = result
|
|
||||||
|
|
||||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
|
||||||
if (!isValidWorkspace) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch existing row to support partial updates
|
|
||||||
const [existingRow] = await db
|
|
||||||
.select({ data: userTableRows.data })
|
|
||||||
.from(userTableRows)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userTableRows.id, rowId),
|
|
||||||
eq(userTableRows.tableId, tableId),
|
|
||||||
eq(userTableRows.workspaceId, validated.workspaceId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
if (!existingRow) {
|
|
||||||
return NextResponse.json({ error: 'Row not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge existing data with incoming partial data (incoming takes precedence)
|
|
||||||
const mergedData = {
|
|
||||||
...(existingRow.data as RowData),
|
|
||||||
...(validated.data as RowData),
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = await validateRowData({
|
|
||||||
rowData: mergedData,
|
|
||||||
schema: table.schema as TableSchema,
|
|
||||||
tableId,
|
|
||||||
excludeRowId: rowId,
|
|
||||||
})
|
|
||||||
if (!validation.valid) return validation.response
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
const [updatedRow] = await db
|
|
||||||
.update(userTableRows)
|
|
||||||
.set({
|
|
||||||
data: mergedData,
|
|
||||||
updatedAt: now,
|
|
||||||
})
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userTableRows.id, rowId),
|
|
||||||
eq(userTableRows.tableId, tableId),
|
|
||||||
eq(userTableRows.workspaceId, validated.workspaceId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.returning()
|
|
||||||
|
|
||||||
if (!updatedRow) {
|
|
||||||
return NextResponse.json({ error: 'Row not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Updated row ${rowId} in table ${tableId}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
row: {
|
|
||||||
id: updatedRow.id,
|
|
||||||
data: updatedRow.data,
|
|
||||||
createdAt: updatedRow.createdAt.toISOString(),
|
|
||||||
updatedAt: updatedRow.updatedAt.toISOString(),
|
|
||||||
},
|
|
||||||
message: 'Row updated successfully',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error updating row:`, error)
|
|
||||||
return NextResponse.json({ error: 'Failed to update row' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** DELETE /api/table/[tableId]/rows/[rowId] - Deletes a single row. */
|
|
||||||
export async function DELETE(request: NextRequest, { params }: RowRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId, rowId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: unknown = await request.json()
|
|
||||||
const validated = DeleteRowSchema.parse(body)
|
|
||||||
|
|
||||||
const result = await checkAccess(tableId, authResult.userId, 'write')
|
|
||||||
if (!result.ok) return accessError(result, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = result
|
|
||||||
|
|
||||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
|
||||||
if (!isValidWorkspace) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const [deletedRow] = await db
|
|
||||||
.delete(userTableRows)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userTableRows.id, rowId),
|
|
||||||
eq(userTableRows.tableId, tableId),
|
|
||||||
eq(userTableRows.workspaceId, validated.workspaceId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.returning()
|
|
||||||
|
|
||||||
if (!deletedRow) {
|
|
||||||
return NextResponse.json({ error: 'Row not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Deleted row ${rowId} from table ${tableId}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
message: 'Row deleted successfully',
|
|
||||||
deletedCount: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error deleting row:`, error)
|
|
||||||
return NextResponse.json({ error: 'Failed to delete row' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,681 +0,0 @@
|
|||||||
import { db } from '@sim/db'
|
|
||||||
import { userTableRows } from '@sim/db/schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { and, eq, sql } from 'drizzle-orm'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
|
||||||
import type { Filter, RowData, Sort, TableSchema } from '@/lib/table'
|
|
||||||
import {
|
|
||||||
checkUniqueConstraintsDb,
|
|
||||||
getUniqueColumns,
|
|
||||||
TABLE_LIMITS,
|
|
||||||
USER_TABLE_ROWS_SQL_NAME,
|
|
||||||
validateBatchRows,
|
|
||||||
validateRowAgainstSchema,
|
|
||||||
validateRowData,
|
|
||||||
validateRowSize,
|
|
||||||
} from '@/lib/table'
|
|
||||||
import { buildFilterClause, buildSortClause } from '@/lib/table/sql'
|
|
||||||
import { accessError, checkAccess } from '../../utils'
|
|
||||||
|
|
||||||
const logger = createLogger('TableRowsAPI')
|
|
||||||
|
|
||||||
const InsertRowSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
data: z.record(z.unknown(), { required_error: 'Row data is required' }),
|
|
||||||
})
|
|
||||||
|
|
||||||
const BatchInsertRowsSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
rows: z
|
|
||||||
.array(z.record(z.unknown()), { required_error: 'Rows array is required' })
|
|
||||||
.min(1, 'At least one row is required')
|
|
||||||
.max(1000, 'Cannot insert more than 1000 rows per batch'),
|
|
||||||
})
|
|
||||||
|
|
||||||
const QueryRowsSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
filter: z.record(z.unknown()).optional(),
|
|
||||||
sort: z.record(z.enum(['asc', 'desc'])).optional(),
|
|
||||||
limit: z.coerce
|
|
||||||
.number({ required_error: 'Limit must be a number' })
|
|
||||||
.int('Limit must be an integer')
|
|
||||||
.min(1, 'Limit must be at least 1')
|
|
||||||
.max(TABLE_LIMITS.MAX_QUERY_LIMIT, `Limit cannot exceed ${TABLE_LIMITS.MAX_QUERY_LIMIT}`)
|
|
||||||
.optional()
|
|
||||||
.default(100),
|
|
||||||
offset: z.coerce
|
|
||||||
.number({ required_error: 'Offset must be a number' })
|
|
||||||
.int('Offset must be an integer')
|
|
||||||
.min(0, 'Offset must be 0 or greater')
|
|
||||||
.optional()
|
|
||||||
.default(0),
|
|
||||||
})
|
|
||||||
|
|
||||||
const UpdateRowsByFilterSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
filter: z.record(z.unknown(), { required_error: 'Filter criteria is required' }),
|
|
||||||
data: z.record(z.unknown(), { required_error: 'Update data is required' }),
|
|
||||||
limit: z.coerce
|
|
||||||
.number({ required_error: 'Limit must be a number' })
|
|
||||||
.int('Limit must be an integer')
|
|
||||||
.min(1, 'Limit must be at least 1')
|
|
||||||
.max(1000, 'Cannot update more than 1000 rows per operation')
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const DeleteRowsByFilterSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
filter: z.record(z.unknown(), { required_error: 'Filter criteria is required' }),
|
|
||||||
limit: z.coerce
|
|
||||||
.number({ required_error: 'Limit must be a number' })
|
|
||||||
.int('Limit must be an integer')
|
|
||||||
.min(1, 'Limit must be at least 1')
|
|
||||||
.max(1000, 'Cannot delete more than 1000 rows per operation')
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface TableRowsRouteParams {
|
|
||||||
params: Promise<{ tableId: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleBatchInsert(
|
|
||||||
requestId: string,
|
|
||||||
tableId: string,
|
|
||||||
body: z.infer<typeof BatchInsertRowsSchema>,
|
|
||||||
userId: string
|
|
||||||
): Promise<NextResponse> {
|
|
||||||
const validated = BatchInsertRowsSchema.parse(body)
|
|
||||||
|
|
||||||
const accessResult = await checkAccess(tableId, userId, 'write')
|
|
||||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = accessResult
|
|
||||||
|
|
||||||
if (validated.workspaceId !== table.workspaceId) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceId = validated.workspaceId
|
|
||||||
|
|
||||||
const remainingCapacity = table.maxRows - table.rowCount
|
|
||||||
if (remainingCapacity < validated.rows.length) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: `Insufficient capacity. Can only insert ${remainingCapacity} more rows (table has ${table.rowCount}/${table.maxRows} rows)`,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = await validateBatchRows({
|
|
||||||
rows: validated.rows as RowData[],
|
|
||||||
schema: table.schema as TableSchema,
|
|
||||||
tableId,
|
|
||||||
})
|
|
||||||
if (!validation.valid) return validation.response
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
const rowsToInsert = validated.rows.map((data) => ({
|
|
||||||
id: `row_${crypto.randomUUID().replace(/-/g, '')}`,
|
|
||||||
tableId,
|
|
||||||
workspaceId,
|
|
||||||
data,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
createdBy: userId,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const insertedRows = await db.insert(userTableRows).values(rowsToInsert).returning()
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Batch inserted ${insertedRows.length} rows into table ${tableId}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
rows: insertedRows.map((r) => ({
|
|
||||||
id: r.id,
|
|
||||||
data: r.data,
|
|
||||||
createdAt: r.createdAt.toISOString(),
|
|
||||||
updatedAt: r.updatedAt.toISOString(),
|
|
||||||
})),
|
|
||||||
insertedCount: insertedRows.length,
|
|
||||||
message: `Successfully inserted ${insertedRows.length} rows`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST /api/table/[tableId]/rows - Inserts row(s). Supports single or batch insert. */
|
|
||||||
export async function POST(request: NextRequest, { params }: TableRowsRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: unknown = await request.json()
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof body === 'object' &&
|
|
||||||
body !== null &&
|
|
||||||
'rows' in body &&
|
|
||||||
Array.isArray((body as Record<string, unknown>).rows)
|
|
||||||
) {
|
|
||||||
return handleBatchInsert(
|
|
||||||
requestId,
|
|
||||||
tableId,
|
|
||||||
body as z.infer<typeof BatchInsertRowsSchema>,
|
|
||||||
authResult.userId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const validated = InsertRowSchema.parse(body)
|
|
||||||
|
|
||||||
const accessResult = await checkAccess(tableId, authResult.userId, 'write')
|
|
||||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = accessResult
|
|
||||||
|
|
||||||
if (validated.workspaceId !== table.workspaceId) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceId = validated.workspaceId
|
|
||||||
const rowData = validated.data as RowData
|
|
||||||
|
|
||||||
const validation = await validateRowData({
|
|
||||||
rowData,
|
|
||||||
schema: table.schema as TableSchema,
|
|
||||||
tableId,
|
|
||||||
})
|
|
||||||
if (!validation.valid) return validation.response
|
|
||||||
|
|
||||||
if (table.rowCount >= table.maxRows) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Table row limit reached (${table.maxRows} rows max)` },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowId = `row_${crypto.randomUUID().replace(/-/g, '')}`
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
const [row] = await db
|
|
||||||
.insert(userTableRows)
|
|
||||||
.values({
|
|
||||||
id: rowId,
|
|
||||||
tableId,
|
|
||||||
workspaceId,
|
|
||||||
data: validated.data,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
createdBy: authResult.userId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Inserted row ${rowId} into table ${tableId}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
row: {
|
|
||||||
id: row.id,
|
|
||||||
data: row.data,
|
|
||||||
createdAt: row.createdAt.toISOString(),
|
|
||||||
updatedAt: row.updatedAt.toISOString(),
|
|
||||||
},
|
|
||||||
message: 'Row inserted successfully',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error inserting row:`, error)
|
|
||||||
return NextResponse.json({ error: 'Failed to insert row' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET /api/table/[tableId]/rows - Queries rows with filtering, sorting, and pagination. */
|
|
||||||
export async function GET(request: NextRequest, { params }: TableRowsRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const workspaceId = searchParams.get('workspaceId')
|
|
||||||
const filterParam = searchParams.get('filter')
|
|
||||||
const sortParam = searchParams.get('sort')
|
|
||||||
const limit = searchParams.get('limit')
|
|
||||||
const offset = searchParams.get('offset')
|
|
||||||
|
|
||||||
let filter: Record<string, unknown> | undefined
|
|
||||||
let sort: Sort | undefined
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (filterParam) {
|
|
||||||
filter = JSON.parse(filterParam) as Record<string, unknown>
|
|
||||||
}
|
|
||||||
if (sortParam) {
|
|
||||||
sort = JSON.parse(sortParam) as Sort
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return NextResponse.json({ error: 'Invalid filter or sort JSON' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const validated = QueryRowsSchema.parse({
|
|
||||||
workspaceId,
|
|
||||||
filter,
|
|
||||||
sort,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
})
|
|
||||||
|
|
||||||
const accessResult = await checkAccess(tableId, authResult.userId, 'read')
|
|
||||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = accessResult
|
|
||||||
|
|
||||||
if (validated.workspaceId !== table.workspaceId) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseConditions = [
|
|
||||||
eq(userTableRows.tableId, tableId),
|
|
||||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (validated.filter) {
|
|
||||||
const filterClause = buildFilterClause(validated.filter as Filter, USER_TABLE_ROWS_SQL_NAME)
|
|
||||||
if (filterClause) {
|
|
||||||
baseConditions.push(filterClause)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = db
|
|
||||||
.select({
|
|
||||||
id: userTableRows.id,
|
|
||||||
data: userTableRows.data,
|
|
||||||
createdAt: userTableRows.createdAt,
|
|
||||||
updatedAt: userTableRows.updatedAt,
|
|
||||||
})
|
|
||||||
.from(userTableRows)
|
|
||||||
.where(and(...baseConditions))
|
|
||||||
|
|
||||||
if (validated.sort) {
|
|
||||||
const schema = table.schema as TableSchema
|
|
||||||
const sortClause = buildSortClause(validated.sort, USER_TABLE_ROWS_SQL_NAME, schema.columns)
|
|
||||||
if (sortClause) {
|
|
||||||
query = query.orderBy(sortClause) as typeof query
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query = query.orderBy(userTableRows.createdAt) as typeof query
|
|
||||||
}
|
|
||||||
|
|
||||||
const countQuery = db
|
|
||||||
.select({ count: sql<number>`count(*)` })
|
|
||||||
.from(userTableRows)
|
|
||||||
.where(and(...baseConditions))
|
|
||||||
|
|
||||||
const [{ count: totalCount }] = await countQuery
|
|
||||||
|
|
||||||
const rows = await query.limit(validated.limit).offset(validated.offset)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] Queried ${rows.length} rows from table ${tableId} (total: ${totalCount})`
|
|
||||||
)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
rows: rows.map((r) => ({
|
|
||||||
id: r.id,
|
|
||||||
data: r.data,
|
|
||||||
createdAt: r.createdAt.toISOString(),
|
|
||||||
updatedAt: r.updatedAt.toISOString(),
|
|
||||||
})),
|
|
||||||
rowCount: rows.length,
|
|
||||||
totalCount: Number(totalCount),
|
|
||||||
limit: validated.limit,
|
|
||||||
offset: validated.offset,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error querying rows:`, error)
|
|
||||||
return NextResponse.json({ error: 'Failed to query rows' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** PUT /api/table/[tableId]/rows - Updates rows matching filter criteria. */
|
|
||||||
export async function PUT(request: NextRequest, { params }: TableRowsRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: unknown = await request.json()
|
|
||||||
const validated = UpdateRowsByFilterSchema.parse(body)
|
|
||||||
|
|
||||||
const accessResult = await checkAccess(tableId, authResult.userId, 'write')
|
|
||||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = accessResult
|
|
||||||
|
|
||||||
if (validated.workspaceId !== table.workspaceId) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = validated.data as RowData
|
|
||||||
|
|
||||||
const sizeValidation = validateRowSize(updateData)
|
|
||||||
if (!sizeValidation.valid) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid row data', details: sizeValidation.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseConditions = [
|
|
||||||
eq(userTableRows.tableId, tableId),
|
|
||||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
|
||||||
]
|
|
||||||
|
|
||||||
const filterClause = buildFilterClause(validated.filter as Filter, USER_TABLE_ROWS_SQL_NAME)
|
|
||||||
if (filterClause) {
|
|
||||||
baseConditions.push(filterClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
let matchingRowsQuery = db
|
|
||||||
.select({
|
|
||||||
id: userTableRows.id,
|
|
||||||
data: userTableRows.data,
|
|
||||||
})
|
|
||||||
.from(userTableRows)
|
|
||||||
.where(and(...baseConditions))
|
|
||||||
|
|
||||||
if (validated.limit) {
|
|
||||||
matchingRowsQuery = matchingRowsQuery.limit(validated.limit) as typeof matchingRowsQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingRows = await matchingRowsQuery
|
|
||||||
|
|
||||||
if (matchingRows.length === 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
message: 'No rows matched the filter criteria',
|
|
||||||
updatedCount: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 200 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchingRows.length > TABLE_LIMITS.MAX_BULK_OPERATION_SIZE) {
|
|
||||||
logger.warn(`[${requestId}] Updating ${matchingRows.length} rows. This may take some time.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const row of matchingRows) {
|
|
||||||
const existingData = row.data as RowData
|
|
||||||
const mergedData = { ...existingData, ...updateData }
|
|
||||||
const rowValidation = validateRowAgainstSchema(mergedData, table.schema as TableSchema)
|
|
||||||
if (!rowValidation.valid) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: 'Updated data does not match schema',
|
|
||||||
details: rowValidation.errors,
|
|
||||||
affectedRowId: row.id,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueColumns = getUniqueColumns(table.schema as TableSchema)
|
|
||||||
if (uniqueColumns.length > 0) {
|
|
||||||
// If updating multiple rows, check that updateData doesn't set any unique column
|
|
||||||
// (would cause all rows to have the same value, violating uniqueness)
|
|
||||||
if (matchingRows.length > 1) {
|
|
||||||
const uniqueColumnsInUpdate = uniqueColumns.filter((col) => col.name in updateData)
|
|
||||||
if (uniqueColumnsInUpdate.length > 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: 'Cannot set unique column values when updating multiple rows',
|
|
||||||
details: [
|
|
||||||
`Columns with unique constraint: ${uniqueColumnsInUpdate.map((c) => c.name).join(', ')}. ` +
|
|
||||||
`Updating ${matchingRows.length} rows with the same value would violate uniqueness.`,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check unique constraints against database for each row
|
|
||||||
for (const row of matchingRows) {
|
|
||||||
const existingData = row.data as RowData
|
|
||||||
const mergedData = { ...existingData, ...updateData }
|
|
||||||
const uniqueValidation = await checkUniqueConstraintsDb(
|
|
||||||
tableId,
|
|
||||||
mergedData,
|
|
||||||
table.schema as TableSchema,
|
|
||||||
row.id
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!uniqueValidation.valid) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: 'Unique constraint violation',
|
|
||||||
details: uniqueValidation.errors,
|
|
||||||
affectedRowId: row.id,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
let totalUpdated = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < matchingRows.length; i += TABLE_LIMITS.UPDATE_BATCH_SIZE) {
|
|
||||||
const batch = matchingRows.slice(i, i + TABLE_LIMITS.UPDATE_BATCH_SIZE)
|
|
||||||
const updatePromises = batch.map((row) => {
|
|
||||||
const existingData = row.data as RowData
|
|
||||||
return trx
|
|
||||||
.update(userTableRows)
|
|
||||||
.set({
|
|
||||||
data: { ...existingData, ...updateData },
|
|
||||||
updatedAt: now,
|
|
||||||
})
|
|
||||||
.where(eq(userTableRows.id, row.id))
|
|
||||||
})
|
|
||||||
await Promise.all(updatePromises)
|
|
||||||
totalUpdated += batch.length
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] Updated batch ${Math.floor(i / TABLE_LIMITS.UPDATE_BATCH_SIZE) + 1} (${totalUpdated}/${matchingRows.length} rows)`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Updated ${matchingRows.length} rows in table ${tableId}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
message: 'Rows updated successfully',
|
|
||||||
updatedCount: matchingRows.length,
|
|
||||||
updatedRowIds: matchingRows.map((r) => r.id),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error updating rows by filter:`, error)
|
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
||||||
const detailedError = `Failed to update rows: ${errorMessage}`
|
|
||||||
|
|
||||||
return NextResponse.json({ error: detailedError }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** DELETE /api/table/[tableId]/rows - Deletes rows matching filter criteria. */
|
|
||||||
export async function DELETE(request: NextRequest, { params }: TableRowsRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: unknown = await request.json()
|
|
||||||
const validated = DeleteRowsByFilterSchema.parse(body)
|
|
||||||
|
|
||||||
const accessResult = await checkAccess(tableId, authResult.userId, 'write')
|
|
||||||
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = accessResult
|
|
||||||
|
|
||||||
if (validated.workspaceId !== table.workspaceId) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseConditions = [
|
|
||||||
eq(userTableRows.tableId, tableId),
|
|
||||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
|
||||||
]
|
|
||||||
|
|
||||||
const filterClause = buildFilterClause(validated.filter as Filter, USER_TABLE_ROWS_SQL_NAME)
|
|
||||||
if (filterClause) {
|
|
||||||
baseConditions.push(filterClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
let matchingRowsQuery = db
|
|
||||||
.select({ id: userTableRows.id })
|
|
||||||
.from(userTableRows)
|
|
||||||
.where(and(...baseConditions))
|
|
||||||
|
|
||||||
if (validated.limit) {
|
|
||||||
matchingRowsQuery = matchingRowsQuery.limit(validated.limit) as typeof matchingRowsQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingRows = await matchingRowsQuery
|
|
||||||
|
|
||||||
if (matchingRows.length === 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
message: 'No rows matched the filter criteria',
|
|
||||||
deletedCount: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 200 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchingRows.length > TABLE_LIMITS.DELETE_BATCH_SIZE) {
|
|
||||||
logger.warn(`[${requestId}] Deleting ${matchingRows.length} rows. This may take some time.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowIds = matchingRows.map((r) => r.id)
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
let totalDeleted = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < rowIds.length; i += TABLE_LIMITS.DELETE_BATCH_SIZE) {
|
|
||||||
const batch = rowIds.slice(i, i + TABLE_LIMITS.DELETE_BATCH_SIZE)
|
|
||||||
await trx.delete(userTableRows).where(
|
|
||||||
and(
|
|
||||||
eq(userTableRows.tableId, tableId),
|
|
||||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
|
||||||
sql`${userTableRows.id} = ANY(ARRAY[${sql.join(
|
|
||||||
batch.map((id) => sql`${id}`),
|
|
||||||
sql`, `
|
|
||||||
)}])`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
totalDeleted += batch.length
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] Deleted batch ${Math.floor(i / TABLE_LIMITS.DELETE_BATCH_SIZE) + 1} (${totalDeleted}/${rowIds.length} rows)`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Deleted ${matchingRows.length} rows from table ${tableId}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
message: 'Rows deleted successfully',
|
|
||||||
deletedCount: matchingRows.length,
|
|
||||||
deletedRowIds: rowIds,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error deleting rows by filter:`, error)
|
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
||||||
const detailedError = `Failed to delete rows: ${errorMessage}`
|
|
||||||
|
|
||||||
return NextResponse.json({ error: detailedError }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import { db } from '@sim/db'
|
|
||||||
import { userTableRows } from '@sim/db/schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { and, eq, or, sql } from 'drizzle-orm'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
|
||||||
import type { RowData, TableSchema } from '@/lib/table'
|
|
||||||
import { getUniqueColumns, validateRowData } from '@/lib/table'
|
|
||||||
import { accessError, checkAccess, verifyTableWorkspace } from '../../../utils'
|
|
||||||
|
|
||||||
const logger = createLogger('TableUpsertAPI')
|
|
||||||
|
|
||||||
const UpsertRowSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
data: z.record(z.unknown(), { required_error: 'Row data is required' }),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface UpsertRouteParams {
|
|
||||||
params: Promise<{ tableId: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST /api/table/[tableId]/rows/upsert - Inserts or updates based on unique columns. */
|
|
||||||
export async function POST(request: NextRequest, { params }: UpsertRouteParams) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { tableId } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: unknown = await request.json()
|
|
||||||
const validated = UpsertRowSchema.parse(body)
|
|
||||||
|
|
||||||
const result = await checkAccess(tableId, authResult.userId, 'write')
|
|
||||||
if (!result.ok) return accessError(result, requestId, tableId)
|
|
||||||
|
|
||||||
const { table } = result
|
|
||||||
|
|
||||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
|
||||||
if (!isValidWorkspace) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
|
|
||||||
)
|
|
||||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = table.schema as TableSchema
|
|
||||||
const rowData = validated.data as RowData
|
|
||||||
|
|
||||||
const validation = await validateRowData({
|
|
||||||
rowData,
|
|
||||||
schema,
|
|
||||||
tableId,
|
|
||||||
checkUnique: false,
|
|
||||||
})
|
|
||||||
if (!validation.valid) return validation.response
|
|
||||||
|
|
||||||
const uniqueColumns = getUniqueColumns(schema)
|
|
||||||
|
|
||||||
if (uniqueColumns.length === 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error:
|
|
||||||
'Upsert requires at least one unique column in the schema. Please add a unique constraint to a column or use insert instead.',
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueFilters = uniqueColumns.map((col) => {
|
|
||||||
const value = rowData[col.name]
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return sql`${userTableRows.data}->>${col.name} = ${String(value)}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const validUniqueFilters = uniqueFilters.filter((f): f is Exclude<typeof f, null> => f !== null)
|
|
||||||
|
|
||||||
if (validUniqueFilters.length === 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: `Upsert requires values for at least one unique field: ${uniqueColumns.map((c) => c.name).join(', ')}`,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [existingRow] = await db
|
|
||||||
.select()
|
|
||||||
.from(userTableRows)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userTableRows.tableId, tableId),
|
|
||||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
|
||||||
or(...validUniqueFilters)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
if (!existingRow && table.rowCount >= table.maxRows) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Table row limit reached (${table.maxRows} rows max)` },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const upsertResult = await db.transaction(async (trx) => {
|
|
||||||
if (existingRow) {
|
|
||||||
const [updatedRow] = await trx
|
|
||||||
.update(userTableRows)
|
|
||||||
.set({
|
|
||||||
data: validated.data,
|
|
||||||
updatedAt: now,
|
|
||||||
})
|
|
||||||
.where(eq(userTableRows.id, existingRow.id))
|
|
||||||
.returning()
|
|
||||||
|
|
||||||
return {
|
|
||||||
row: updatedRow,
|
|
||||||
operation: 'update' as const,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [insertedRow] = await trx
|
|
||||||
.insert(userTableRows)
|
|
||||||
.values({
|
|
||||||
id: `row_${crypto.randomUUID().replace(/-/g, '')}`,
|
|
||||||
tableId,
|
|
||||||
workspaceId: validated.workspaceId,
|
|
||||||
data: validated.data,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
createdBy: authResult.userId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
|
|
||||||
return {
|
|
||||||
row: insertedRow,
|
|
||||||
operation: 'insert' as const,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] Upserted (${upsertResult.operation}) row ${upsertResult.row.id} in table ${tableId}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
row: {
|
|
||||||
id: upsertResult.row.id,
|
|
||||||
data: upsertResult.row.data,
|
|
||||||
createdAt: upsertResult.row.createdAt.toISOString(),
|
|
||||||
updatedAt: upsertResult.row.updatedAt.toISOString(),
|
|
||||||
},
|
|
||||||
operation: upsertResult.operation,
|
|
||||||
message: `Row ${upsertResult.operation === 'update' ? 'updated' : 'inserted'} successfully`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error upserting row:`, error)
|
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
||||||
const detailedError = `Failed to upsert row: ${errorMessage}`
|
|
||||||
|
|
||||||
return NextResponse.json({ error: detailedError }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
import { db } from '@sim/db'
|
|
||||||
import { permissions, workspace } from '@sim/db/schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { and, eq } from 'drizzle-orm'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
|
||||||
import {
|
|
||||||
canCreateTable,
|
|
||||||
createTable,
|
|
||||||
getWorkspaceTableLimits,
|
|
||||||
listTables,
|
|
||||||
TABLE_LIMITS,
|
|
||||||
type TableSchema,
|
|
||||||
} from '@/lib/table'
|
|
||||||
import { normalizeColumn } from './utils'
|
|
||||||
|
|
||||||
const logger = createLogger('TableAPI')
|
|
||||||
|
|
||||||
const ColumnSchema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(1, 'Column name is required')
|
|
||||||
.max(
|
|
||||||
TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH,
|
|
||||||
`Column name must be ${TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH} characters or less`
|
|
||||||
)
|
|
||||||
.regex(
|
|
||||||
/^[a-z_][a-z0-9_]*$/i,
|
|
||||||
'Column name must start with a letter or underscore and contain only alphanumeric characters and underscores'
|
|
||||||
),
|
|
||||||
type: z.enum(['string', 'number', 'boolean', 'date', 'json'], {
|
|
||||||
errorMap: () => ({
|
|
||||||
message: 'Column type must be one of: string, number, boolean, date, json',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
required: z.boolean().optional().default(false),
|
|
||||||
unique: z.boolean().optional().default(false),
|
|
||||||
})
|
|
||||||
|
|
||||||
const CreateTableSchema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(1, 'Table name is required')
|
|
||||||
.max(
|
|
||||||
TABLE_LIMITS.MAX_TABLE_NAME_LENGTH,
|
|
||||||
`Table name must be ${TABLE_LIMITS.MAX_TABLE_NAME_LENGTH} characters or less`
|
|
||||||
)
|
|
||||||
.regex(
|
|
||||||
/^[a-z_][a-z0-9_]*$/i,
|
|
||||||
'Table name must start with a letter or underscore and contain only alphanumeric characters and underscores'
|
|
||||||
),
|
|
||||||
description: z
|
|
||||||
.string()
|
|
||||||
.max(
|
|
||||||
TABLE_LIMITS.MAX_DESCRIPTION_LENGTH,
|
|
||||||
`Description must be ${TABLE_LIMITS.MAX_DESCRIPTION_LENGTH} characters or less`
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
schema: z.object({
|
|
||||||
columns: z
|
|
||||||
.array(ColumnSchema)
|
|
||||||
.min(1, 'Table must have at least one column')
|
|
||||||
.max(
|
|
||||||
TABLE_LIMITS.MAX_COLUMNS_PER_TABLE,
|
|
||||||
`Table cannot have more than ${TABLE_LIMITS.MAX_COLUMNS_PER_TABLE} columns`
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
})
|
|
||||||
|
|
||||||
const ListTablesSchema = z.object({
|
|
||||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface WorkspaceAccessResult {
|
|
||||||
hasAccess: boolean
|
|
||||||
canWrite: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkWorkspaceAccess(
|
|
||||||
workspaceId: string,
|
|
||||||
userId: string
|
|
||||||
): Promise<WorkspaceAccessResult> {
|
|
||||||
const [workspaceData] = await db
|
|
||||||
.select({
|
|
||||||
id: workspace.id,
|
|
||||||
ownerId: workspace.ownerId,
|
|
||||||
})
|
|
||||||
.from(workspace)
|
|
||||||
.where(eq(workspace.id, workspaceId))
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
if (!workspaceData) {
|
|
||||||
return { hasAccess: false, canWrite: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workspaceData.ownerId === userId) {
|
|
||||||
return { hasAccess: true, canWrite: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
const [permission] = await db
|
|
||||||
.select({
|
|
||||||
permissionType: permissions.permissionType,
|
|
||||||
})
|
|
||||||
.from(permissions)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(permissions.userId, userId),
|
|
||||||
eq(permissions.entityType, 'workspace'),
|
|
||||||
eq(permissions.entityId, workspaceId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
if (!permission) {
|
|
||||||
return { hasAccess: false, canWrite: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const canWrite = permission.permissionType === 'admin' || permission.permissionType === 'write'
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasAccess: true,
|
|
||||||
canWrite,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST /api/table - Creates a new user-defined table. */
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: unknown = await request.json()
|
|
||||||
const params = CreateTableSchema.parse(body)
|
|
||||||
|
|
||||||
const { hasAccess, canWrite } = await checkWorkspaceAccess(
|
|
||||||
params.workspaceId,
|
|
||||||
authResult.userId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!hasAccess || !canWrite) {
|
|
||||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check billing plan limits
|
|
||||||
const existingTables = await listTables(params.workspaceId)
|
|
||||||
const { canCreate, maxTables } = await canCreateTable(params.workspaceId, existingTables.length)
|
|
||||||
|
|
||||||
if (!canCreate) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: `Workspace has reached the maximum table limit (${maxTables}) for your plan. Please upgrade to create more tables.`,
|
|
||||||
},
|
|
||||||
{ status: 403 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get plan-based row limits
|
|
||||||
const planLimits = await getWorkspaceTableLimits(params.workspaceId)
|
|
||||||
const maxRowsPerTable = planLimits.maxRowsPerTable
|
|
||||||
|
|
||||||
const normalizedSchema: TableSchema = {
|
|
||||||
columns: params.schema.columns.map(normalizeColumn),
|
|
||||||
}
|
|
||||||
|
|
||||||
const table = await createTable(
|
|
||||||
{
|
|
||||||
name: params.name,
|
|
||||||
description: params.description,
|
|
||||||
schema: normalizedSchema,
|
|
||||||
workspaceId: params.workspaceId,
|
|
||||||
userId: authResult.userId,
|
|
||||||
maxRows: maxRowsPerTable,
|
|
||||||
},
|
|
||||||
requestId
|
|
||||||
)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
table: {
|
|
||||||
id: table.id,
|
|
||||||
name: table.name,
|
|
||||||
description: table.description,
|
|
||||||
schema: table.schema,
|
|
||||||
rowCount: table.rowCount,
|
|
||||||
maxRows: table.maxRows,
|
|
||||||
createdAt:
|
|
||||||
table.createdAt instanceof Date
|
|
||||||
? table.createdAt.toISOString()
|
|
||||||
: String(table.createdAt),
|
|
||||||
updatedAt:
|
|
||||||
table.updatedAt instanceof Date
|
|
||||||
? table.updatedAt.toISOString()
|
|
||||||
: String(table.updatedAt),
|
|
||||||
},
|
|
||||||
message: 'Table created successfully',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (
|
|
||||||
error.message.includes('Invalid table name') ||
|
|
||||||
error.message.includes('Invalid schema') ||
|
|
||||||
error.message.includes('already exists') ||
|
|
||||||
error.message.includes('maximum table limit')
|
|
||||||
) {
|
|
||||||
return NextResponse.json({ error: error.message }, { status: 400 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error creating table:`, error)
|
|
||||||
return NextResponse.json({ error: 'Failed to create table' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET /api/table - Lists all tables in a workspace. */
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkHybridAuth(request)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const workspaceId = searchParams.get('workspaceId')
|
|
||||||
|
|
||||||
const validation = ListTablesSchema.safeParse({ workspaceId })
|
|
||||||
if (!validation.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: validation.error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = validation.data
|
|
||||||
|
|
||||||
const { hasAccess } = await checkWorkspaceAccess(params.workspaceId, authResult.userId)
|
|
||||||
|
|
||||||
if (!hasAccess) {
|
|
||||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const tables = await listTables(params.workspaceId)
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Listed ${tables.length} tables in workspace ${params.workspaceId}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
tables: tables.map((t) => {
|
|
||||||
const schemaData = t.schema as TableSchema
|
|
||||||
return {
|
|
||||||
...t,
|
|
||||||
schema: {
|
|
||||||
columns: schemaData.columns.map(normalizeColumn),
|
|
||||||
},
|
|
||||||
createdAt:
|
|
||||||
t.createdAt instanceof Date ? t.createdAt.toISOString() : String(t.createdAt),
|
|
||||||
updatedAt:
|
|
||||||
t.updatedAt instanceof Date ? t.updatedAt.toISOString() : String(t.updatedAt),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
totalCount: tables.length,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validation error', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error listing tables:`, error)
|
|
||||||
return NextResponse.json({ error: 'Failed to list tables' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { NextResponse } from 'next/server'
|
|
||||||
import type { ColumnDefinition, TableDefinition } from '@/lib/table'
|
|
||||||
import { getTableById } from '@/lib/table'
|
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
|
||||||
|
|
||||||
const logger = createLogger('TableUtils')
|
|
||||||
|
|
||||||
export interface TableAccessResult {
|
|
||||||
hasAccess: true
|
|
||||||
table: TableDefinition
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TableAccessDenied {
|
|
||||||
hasAccess: false
|
|
||||||
notFound?: boolean
|
|
||||||
reason?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TableAccessCheck = TableAccessResult | TableAccessDenied
|
|
||||||
|
|
||||||
export type AccessResult = { ok: true; table: TableDefinition } | { ok: false; status: 404 | 403 }
|
|
||||||
|
|
||||||
export interface ApiErrorResponse {
|
|
||||||
error: string
|
|
||||||
details?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a user has read access to a table.
|
|
||||||
* Read access is granted if:
|
|
||||||
* 1. User created the table, OR
|
|
||||||
* 2. User has any permission on the table's workspace (read, write, or admin)
|
|
||||||
*
|
|
||||||
* Follows the same pattern as Knowledge Base access checks.
|
|
||||||
*/
|
|
||||||
export async function checkTableAccess(tableId: string, userId: string): Promise<TableAccessCheck> {
|
|
||||||
const table = await getTableById(tableId)
|
|
||||||
|
|
||||||
if (!table) {
|
|
||||||
return { hasAccess: false, notFound: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 1: User created the table
|
|
||||||
if (table.createdBy === userId) {
|
|
||||||
return { hasAccess: true, table }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 2: Table belongs to a workspace the user has permissions for
|
|
||||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', table.workspaceId)
|
|
||||||
if (userPermission !== null) {
|
|
||||||
return { hasAccess: true, table }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { hasAccess: false, reason: 'User does not have access to this table' }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a user has write access to a table.
|
|
||||||
* Write access is granted if:
|
|
||||||
* 1. User created the table, OR
|
|
||||||
* 2. User has write or admin permissions on the table's workspace
|
|
||||||
*
|
|
||||||
* Follows the same pattern as Knowledge Base write access checks.
|
|
||||||
*/
|
|
||||||
export async function checkTableWriteAccess(
|
|
||||||
tableId: string,
|
|
||||||
userId: string
|
|
||||||
): Promise<TableAccessCheck> {
|
|
||||||
const table = await getTableById(tableId)
|
|
||||||
|
|
||||||
if (!table) {
|
|
||||||
return { hasAccess: false, notFound: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 1: User created the table
|
|
||||||
if (table.createdBy === userId) {
|
|
||||||
return { hasAccess: true, table }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 2: Table belongs to a workspace and user has write/admin permissions
|
|
||||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', table.workspaceId)
|
|
||||||
if (userPermission === 'write' || userPermission === 'admin') {
|
|
||||||
return { hasAccess: true, table }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { hasAccess: false, reason: 'User does not have write access to this table' }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use checkTableAccess or checkTableWriteAccess instead.
|
|
||||||
* Legacy access check function for backwards compatibility.
|
|
||||||
*/
|
|
||||||
export async function checkAccess(
|
|
||||||
tableId: string,
|
|
||||||
userId: string,
|
|
||||||
level: 'read' | 'write' | 'admin' = 'read'
|
|
||||||
): Promise<AccessResult> {
|
|
||||||
const table = await getTableById(tableId)
|
|
||||||
|
|
||||||
if (!table) {
|
|
||||||
return { ok: false, status: 404 }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (table.createdBy === userId) {
|
|
||||||
return { ok: true, table }
|
|
||||||
}
|
|
||||||
|
|
||||||
const permission = await getUserEntityPermissions(userId, 'workspace', table.workspaceId)
|
|
||||||
const hasAccess =
|
|
||||||
permission !== null &&
|
|
||||||
(level === 'read' ||
|
|
||||||
(level === 'write' && (permission === 'write' || permission === 'admin')) ||
|
|
||||||
(level === 'admin' && permission === 'admin'))
|
|
||||||
|
|
||||||
return hasAccess ? { ok: true, table } : { ok: false, status: 403 }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function accessError(
|
|
||||||
result: { ok: false; status: 404 | 403 },
|
|
||||||
requestId: string,
|
|
||||||
context?: string
|
|
||||||
): NextResponse {
|
|
||||||
const message = result.status === 404 ? 'Table not found' : 'Access denied'
|
|
||||||
logger.warn(`[${requestId}] ${message}${context ? `: ${context}` : ''}`)
|
|
||||||
return NextResponse.json({ error: message }, { status: result.status })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a TableAccessDenied result to an appropriate HTTP response.
|
|
||||||
* Use with checkTableAccess or checkTableWriteAccess.
|
|
||||||
*/
|
|
||||||
export function tableAccessError(
|
|
||||||
result: TableAccessDenied,
|
|
||||||
requestId: string,
|
|
||||||
context?: string
|
|
||||||
): NextResponse {
|
|
||||||
const status = result.notFound ? 404 : 403
|
|
||||||
const message = result.notFound ? 'Table not found' : (result.reason ?? 'Access denied')
|
|
||||||
logger.warn(`[${requestId}] ${message}${context ? `: ${context}` : ''}`)
|
|
||||||
return NextResponse.json({ error: message }, { status })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifyTableWorkspace(tableId: string, workspaceId: string): Promise<boolean> {
|
|
||||||
const table = await getTableById(tableId)
|
|
||||||
return table?.workspaceId === workspaceId
|
|
||||||
}
|
|
||||||
|
|
||||||
export function errorResponse(
|
|
||||||
message: string,
|
|
||||||
status: number,
|
|
||||||
details?: unknown
|
|
||||||
): NextResponse<ApiErrorResponse> {
|
|
||||||
const body: ApiErrorResponse = { error: message }
|
|
||||||
if (details !== undefined) {
|
|
||||||
body.details = details
|
|
||||||
}
|
|
||||||
return NextResponse.json(body, { status })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function badRequestResponse(message: string, details?: unknown) {
|
|
||||||
return errorResponse(message, 400, details)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unauthorizedResponse(message = 'Authentication required') {
|
|
||||||
return errorResponse(message, 401)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function forbiddenResponse(message = 'Access denied') {
|
|
||||||
return errorResponse(message, 403)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notFoundResponse(message = 'Resource not found') {
|
|
||||||
return errorResponse(message, 404)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serverErrorResponse(message = 'Internal server error') {
|
|
||||||
return errorResponse(message, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeColumn(col: ColumnDefinition): ColumnDefinition {
|
|
||||||
return {
|
|
||||||
name: col.name,
|
|
||||||
type: col.type,
|
|
||||||
required: col.required ?? false,
|
|
||||||
unique: col.unique ?? false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}`)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user