mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-19 02:34:37 -05:00
Compare commits
20 Commits
feat/mothe
...
v0.5.93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdca73679d | ||
|
|
86ca984926 | ||
|
|
e3964624ac | ||
|
|
7c7c0fd955 | ||
|
|
e37b4a926d | ||
|
|
11f3a14c02 | ||
|
|
eab01e0272 | ||
|
|
bbcef7ce5c | ||
|
|
0ee52df5a7 | ||
|
|
6421b1a0ca | ||
|
|
61a5c98717 | ||
|
|
da46a387c9 | ||
|
|
a0afb5d03e | ||
|
|
cdacb796a8 | ||
|
|
3ce54147e6 | ||
|
|
08690b2906 | ||
|
|
299cc26694 | ||
|
|
48715ff013 | ||
|
|
ad0d0ed1f1 | ||
|
|
b7e377ec4b |
@@ -59,12 +59,6 @@ body {
|
|||||||
--content-gap: 1.75rem;
|
--content-gap: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove custom layout variable overrides to fallback to fumadocs defaults */
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
Navbar Light Mode Styling
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
/* Light mode navbar and search styling */
|
/* Light mode navbar and search styling */
|
||||||
:root:not(.dark) nav {
|
:root:not(.dark) nav {
|
||||||
background-color: hsla(0, 0%, 96%, 0.85) !important;
|
background-color: hsla(0, 0%, 96%, 0.85) !important;
|
||||||
@@ -88,10 +82,6 @@ body {
|
|||||||
-webkit-backdrop-filter: blur(25px) saturate(180%) brightness(0.6) !important;
|
-webkit-backdrop-filter: blur(25px) saturate(180%) brightness(0.6) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
Custom Sidebar Styling (Turborepo-inspired)
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
/* Floating sidebar appearance - remove background */
|
/* Floating sidebar appearance - remove background */
|
||||||
[data-sidebar-container],
|
[data-sidebar-container],
|
||||||
#nd-sidebar {
|
#nd-sidebar {
|
||||||
@@ -468,10 +458,6 @@ aside[data-sidebar],
|
|||||||
writing-mode: horizontal-tb !important;
|
writing-mode: horizontal-tb !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
Code Block Styling (Improved)
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
/* Apply Geist Mono to code elements */
|
/* Apply Geist Mono to code elements */
|
||||||
code,
|
code,
|
||||||
pre,
|
pre,
|
||||||
@@ -532,10 +518,6 @@ pre code .line {
|
|||||||
color: var(--color-fd-primary);
|
color: var(--color-fd-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
TOC (Table of Contents) Styling
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
/* Remove the thin border-left on nested TOC items (keeps main indicator only) */
|
/* Remove the thin border-left on nested TOC items (keeps main indicator only) */
|
||||||
#nd-toc a[style*="padding-inline-start"] {
|
#nd-toc a[style*="padding-inline-start"] {
|
||||||
border-left: none !important;
|
border-left: none !important;
|
||||||
@@ -554,10 +536,6 @@ main article,
|
|||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
Center and Constrain Main Content Width
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
/* Main content area - center and constrain like turborepo/raindrop */
|
/* Main content area - center and constrain like turborepo/raindrop */
|
||||||
/* Note: --sidebar-offset and --toc-offset are now applied at #nd-docs-layout level */
|
/* Note: --sidebar-offset and --toc-offset are now applied at #nd-docs-layout level */
|
||||||
main[data-main] {
|
main[data-main] {
|
||||||
|
|||||||
@@ -234,7 +234,6 @@ List actions from incident.io. Optionally filter by incident ID.
|
|||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `apiKey` | string | Yes | incident.io API Key |
|
| `apiKey` | string | Yes | incident.io API Key |
|
||||||
| `incident_id` | string | No | Filter actions by incident ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) |
|
| `incident_id` | string | No | Filter actions by incident ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) |
|
||||||
| `page_size` | number | No | Number of actions to return per page \(e.g., 10, 25, 50\) |
|
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -309,7 +308,6 @@ List follow-ups from incident.io. Optionally filter by incident ID.
|
|||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `apiKey` | string | Yes | incident.io API Key |
|
| `apiKey` | string | Yes | incident.io API Key |
|
||||||
| `incident_id` | string | No | Filter follow-ups by incident ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) |
|
| `incident_id` | string | No | Filter follow-ups by incident ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) |
|
||||||
| `page_size` | number | No | Number of follow-ups to return per page \(e.g., 10, 25, 50\) |
|
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -396,6 +394,7 @@ List all users in your Incident.io workspace. Returns user details including id,
|
|||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `apiKey` | string | Yes | Incident.io API Key |
|
| `apiKey` | string | Yes | Incident.io API Key |
|
||||||
| `page_size` | number | No | Number of results to return per page \(e.g., 10, 25, 50\). Default: 25 |
|
| `page_size` | number | No | Number of results to return per page \(e.g., 10, 25, 50\). Default: 25 |
|
||||||
|
| `after` | string | No | Pagination cursor to fetch the next page of results |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -406,6 +405,10 @@ List all users in your Incident.io workspace. Returns user details including id,
|
|||||||
| ↳ `name` | string | Full name of the user |
|
| ↳ `name` | string | Full name of the user |
|
||||||
| ↳ `email` | string | Email address of the user |
|
| ↳ `email` | string | Email address of the user |
|
||||||
| ↳ `role` | string | Role of the user in the workspace |
|
| ↳ `role` | string | Role of the user in the workspace |
|
||||||
|
| `pagination_meta` | object | Pagination metadata |
|
||||||
|
| ↳ `after` | string | Cursor for next page |
|
||||||
|
| ↳ `page_size` | number | Number of items per page |
|
||||||
|
| ↳ `total_record_count` | number | Total number of records |
|
||||||
|
|
||||||
### `incidentio_users_show`
|
### `incidentio_users_show`
|
||||||
|
|
||||||
@@ -644,7 +647,6 @@ List all escalation policies in incident.io
|
|||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `apiKey` | string | Yes | incident.io API Key |
|
| `apiKey` | string | Yes | incident.io API Key |
|
||||||
| `page_size` | number | No | Number of results per page \(e.g., 10, 25, 50\). Default: 25 |
|
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ Retrieve all deals from Pipedrive with optional filters
|
|||||||
| `pipeline_id` | string | No | If supplied, only deals in the specified pipeline are returned \(e.g., "1"\) |
|
| `pipeline_id` | string | No | If supplied, only deals in the specified pipeline are returned \(e.g., "1"\) |
|
||||||
| `updated_since` | string | No | If set, only deals updated after this time are returned. Format: 2025-01-01T10:20:00Z |
|
| `updated_since` | string | No | If set, only deals updated after this time are returned. Format: 2025-01-01T10:20:00Z |
|
||||||
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
||||||
|
| `cursor` | string | No | For pagination, the marker representing the first item on the next page |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -74,6 +75,8 @@ Retrieve all deals from Pipedrive with optional filters
|
|||||||
| `metadata` | object | Pagination metadata for the response |
|
| `metadata` | object | Pagination metadata for the response |
|
||||||
| ↳ `total_items` | number | Total number of items |
|
| ↳ `total_items` | number | Total number of items |
|
||||||
| ↳ `has_more` | boolean | Whether more items are available |
|
| ↳ `has_more` | boolean | Whether more items are available |
|
||||||
|
| ↳ `next_cursor` | string | Cursor for fetching the next page \(v2 endpoints\) |
|
||||||
|
| ↳ `next_start` | number | Offset for fetching the next page \(v1 endpoints\) |
|
||||||
| `success` | boolean | Operation success status |
|
| `success` | boolean | Operation success status |
|
||||||
|
|
||||||
### `pipedrive_get_deal`
|
### `pipedrive_get_deal`
|
||||||
@@ -148,10 +151,9 @@ Retrieve files from Pipedrive with optional filters
|
|||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `deal_id` | string | No | Filter files by deal ID \(e.g., "123"\) |
|
| `sort` | string | No | Sort files by field \(supported: "id", "update_time"\) |
|
||||||
| `person_id` | string | No | Filter files by person ID \(e.g., "456"\) |
|
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 100\) |
|
||||||
| `org_id` | string | No | Filter files by organization ID \(e.g., "789"\) |
|
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
|
||||||
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
|
||||||
| `downloadFiles` | boolean | No | Download file contents into file outputs |
|
| `downloadFiles` | boolean | No | Download file contents into file outputs |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
@@ -171,6 +173,8 @@ Retrieve files from Pipedrive with optional filters
|
|||||||
| ↳ `url` | string | File download URL |
|
| ↳ `url` | string | File download URL |
|
||||||
| `downloadedFiles` | file[] | Downloaded files from Pipedrive |
|
| `downloadedFiles` | file[] | Downloaded files from Pipedrive |
|
||||||
| `total_items` | number | Total number of files returned |
|
| `total_items` | number | Total number of files returned |
|
||||||
|
| `has_more` | boolean | Whether more files are available |
|
||||||
|
| `next_start` | number | Offset for fetching the next page |
|
||||||
| `success` | boolean | Operation success status |
|
| `success` | boolean | Operation success status |
|
||||||
|
|
||||||
### `pipedrive_get_mail_messages`
|
### `pipedrive_get_mail_messages`
|
||||||
@@ -183,6 +187,7 @@ Retrieve mail threads from Pipedrive mailbox
|
|||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `folder` | string | No | Filter by folder: inbox, drafts, sent, archive \(default: inbox\) |
|
| `folder` | string | No | Filter by folder: inbox, drafts, sent, archive \(default: inbox\) |
|
||||||
| `limit` | string | No | Number of results to return \(e.g., "25", default: 50\) |
|
| `limit` | string | No | Number of results to return \(e.g., "25", default: 50\) |
|
||||||
|
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -190,6 +195,8 @@ Retrieve mail threads from Pipedrive mailbox
|
|||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `messages` | array | Array of mail thread objects from Pipedrive mailbox |
|
| `messages` | array | Array of mail thread objects from Pipedrive mailbox |
|
||||||
| `total_items` | number | Total number of mail threads returned |
|
| `total_items` | number | Total number of mail threads returned |
|
||||||
|
| `has_more` | boolean | Whether more messages are available |
|
||||||
|
| `next_start` | number | Offset for fetching the next page |
|
||||||
| `success` | boolean | Operation success status |
|
| `success` | boolean | Operation success status |
|
||||||
|
|
||||||
### `pipedrive_get_mail_thread`
|
### `pipedrive_get_mail_thread`
|
||||||
@@ -221,7 +228,7 @@ Retrieve all pipelines from Pipedrive
|
|||||||
| `sort_by` | string | No | Field to sort by: id, update_time, add_time \(default: id\) |
|
| `sort_by` | string | No | Field to sort by: id, update_time, add_time \(default: id\) |
|
||||||
| `sort_direction` | string | No | Sorting direction: asc, desc \(default: asc\) |
|
| `sort_direction` | string | No | Sorting direction: asc, desc \(default: asc\) |
|
||||||
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
||||||
| `cursor` | string | No | For pagination, the marker representing the first item on the next page |
|
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -237,6 +244,8 @@ Retrieve all pipelines from Pipedrive
|
|||||||
| ↳ `add_time` | string | When the pipeline was created |
|
| ↳ `add_time` | string | When the pipeline was created |
|
||||||
| ↳ `update_time` | string | When the pipeline was last updated |
|
| ↳ `update_time` | string | When the pipeline was last updated |
|
||||||
| `total_items` | number | Total number of pipelines returned |
|
| `total_items` | number | Total number of pipelines returned |
|
||||||
|
| `has_more` | boolean | Whether more pipelines are available |
|
||||||
|
| `next_start` | number | Offset for fetching the next page |
|
||||||
| `success` | boolean | Operation success status |
|
| `success` | boolean | Operation success status |
|
||||||
|
|
||||||
### `pipedrive_get_pipeline_deals`
|
### `pipedrive_get_pipeline_deals`
|
||||||
@@ -249,8 +258,8 @@ Retrieve all deals in a specific pipeline
|
|||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `pipeline_id` | string | Yes | The ID of the pipeline \(e.g., "1"\) |
|
| `pipeline_id` | string | Yes | The ID of the pipeline \(e.g., "1"\) |
|
||||||
| `stage_id` | string | No | Filter by specific stage within the pipeline \(e.g., "2"\) |
|
| `stage_id` | string | No | Filter by specific stage within the pipeline \(e.g., "2"\) |
|
||||||
| `status` | string | No | Filter by deal status: open, won, lost |
|
|
||||||
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
||||||
|
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -271,6 +280,7 @@ Retrieve all projects or a specific project from Pipedrive
|
|||||||
| `project_id` | string | No | Optional: ID of a specific project to retrieve \(e.g., "123"\) |
|
| `project_id` | string | No | Optional: ID of a specific project to retrieve \(e.g., "123"\) |
|
||||||
| `status` | string | No | Filter by project status: open, completed, deleted \(only for listing all\) |
|
| `status` | string | No | Filter by project status: open, completed, deleted \(only for listing all\) |
|
||||||
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500, only for listing all\) |
|
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500, only for listing all\) |
|
||||||
|
| `cursor` | string | No | For pagination, the marker representing the first item on the next page |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -279,6 +289,8 @@ Retrieve all projects or a specific project from Pipedrive
|
|||||||
| `projects` | array | Array of project objects \(when listing all\) |
|
| `projects` | array | Array of project objects \(when listing all\) |
|
||||||
| `project` | object | Single project object \(when project_id is provided\) |
|
| `project` | object | Single project object \(when project_id is provided\) |
|
||||||
| `total_items` | number | Total number of projects returned |
|
| `total_items` | number | Total number of projects returned |
|
||||||
|
| `has_more` | boolean | Whether more projects are available |
|
||||||
|
| `next_cursor` | string | Cursor for fetching the next page |
|
||||||
| `success` | boolean | Operation success status |
|
| `success` | boolean | Operation success status |
|
||||||
|
|
||||||
### `pipedrive_create_project`
|
### `pipedrive_create_project`
|
||||||
@@ -309,12 +321,11 @@ Retrieve activities (tasks) from Pipedrive with optional filters
|
|||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `deal_id` | string | No | Filter activities by deal ID \(e.g., "123"\) |
|
| `user_id` | string | No | Filter activities by user ID \(e.g., "123"\) |
|
||||||
| `person_id` | string | No | Filter activities by person ID \(e.g., "456"\) |
|
|
||||||
| `org_id` | string | No | Filter activities by organization ID \(e.g., "789"\) |
|
|
||||||
| `type` | string | No | Filter by activity type \(call, meeting, task, deadline, email, lunch\) |
|
| `type` | string | No | Filter by activity type \(call, meeting, task, deadline, email, lunch\) |
|
||||||
| `done` | string | No | Filter by completion status: 0 for not done, 1 for done |
|
| `done` | string | No | Filter by completion status: 0 for not done, 1 for done |
|
||||||
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
||||||
|
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -335,6 +346,8 @@ Retrieve activities (tasks) from Pipedrive with optional filters
|
|||||||
| ↳ `add_time` | string | When the activity was created |
|
| ↳ `add_time` | string | When the activity was created |
|
||||||
| ↳ `update_time` | string | When the activity was last updated |
|
| ↳ `update_time` | string | When the activity was last updated |
|
||||||
| `total_items` | number | Total number of activities returned |
|
| `total_items` | number | Total number of activities returned |
|
||||||
|
| `has_more` | boolean | Whether more activities are available |
|
||||||
|
| `next_start` | number | Offset for fetching the next page |
|
||||||
| `success` | boolean | Operation success status |
|
| `success` | boolean | Operation success status |
|
||||||
|
|
||||||
### `pipedrive_create_activity`
|
### `pipedrive_create_activity`
|
||||||
@@ -399,6 +412,7 @@ Retrieve all leads or a specific lead from Pipedrive
|
|||||||
| `person_id` | string | No | Filter by person ID \(e.g., "456"\) |
|
| `person_id` | string | No | Filter by person ID \(e.g., "456"\) |
|
||||||
| `organization_id` | string | No | Filter by organization ID \(e.g., "789"\) |
|
| `organization_id` | string | No | Filter by organization ID \(e.g., "789"\) |
|
||||||
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
||||||
|
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -433,6 +447,8 @@ Retrieve all leads or a specific lead from Pipedrive
|
|||||||
| ↳ `add_time` | string | When the lead was created \(ISO 8601\) |
|
| ↳ `add_time` | string | When the lead was created \(ISO 8601\) |
|
||||||
| ↳ `update_time` | string | When the lead was last updated \(ISO 8601\) |
|
| ↳ `update_time` | string | When the lead was last updated \(ISO 8601\) |
|
||||||
| `total_items` | number | Total number of leads returned |
|
| `total_items` | number | Total number of leads returned |
|
||||||
|
| `has_more` | boolean | Whether more leads are available |
|
||||||
|
| `next_start` | number | Offset for fetching the next page |
|
||||||
| `success` | boolean | Operation success status |
|
| `success` | boolean | Operation success status |
|
||||||
|
|
||||||
### `pipedrive_create_lead`
|
### `pipedrive_create_lead`
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ Query data from a Supabase table
|
|||||||
| `filter` | string | No | PostgREST filter \(e.g., "id=eq.123"\) |
|
| `filter` | string | No | PostgREST filter \(e.g., "id=eq.123"\) |
|
||||||
| `orderBy` | string | No | Column to order by \(add DESC for descending\) |
|
| `orderBy` | string | No | Column to order by \(add DESC for descending\) |
|
||||||
| `limit` | number | No | Maximum number of rows to return |
|
| `limit` | number | No | Maximum number of rows to return |
|
||||||
|
| `offset` | number | No | Number of rows to skip \(for pagination\) |
|
||||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
@@ -211,6 +212,7 @@ Perform full-text search on a Supabase table
|
|||||||
| `searchType` | string | No | Search type: plain, phrase, or websearch \(default: websearch\) |
|
| `searchType` | string | No | Search type: plain, phrase, or websearch \(default: websearch\) |
|
||||||
| `language` | string | No | Language for text search configuration \(default: english\) |
|
| `language` | string | No | Language for text search configuration \(default: english\) |
|
||||||
| `limit` | number | No | Maximum number of rows to return |
|
| `limit` | number | No | Maximum number of rows to return |
|
||||||
|
| `offset` | number | No | Number of rows to skip \(for pagination\) |
|
||||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ Retrieve form responses from Typeform
|
|||||||
| `formId` | string | Yes | Typeform form ID \(e.g., "abc123XYZ"\) |
|
| `formId` | string | Yes | Typeform form ID \(e.g., "abc123XYZ"\) |
|
||||||
| `apiKey` | string | Yes | Typeform Personal Access Token |
|
| `apiKey` | string | Yes | Typeform Personal Access Token |
|
||||||
| `pageSize` | number | No | Number of responses to retrieve \(e.g., 10, 25, 50\) |
|
| `pageSize` | number | No | Number of responses to retrieve \(e.g., 10, 25, 50\) |
|
||||||
|
| `before` | string | No | Cursor token for fetching the next page of older responses |
|
||||||
|
| `after` | string | No | Cursor token for fetching the next page of newer responses |
|
||||||
| `since` | string | No | Retrieve responses submitted after this date \(e.g., "2024-01-01T00:00:00Z"\) |
|
| `since` | string | No | Retrieve responses submitted after this date \(e.g., "2024-01-01T00:00:00Z"\) |
|
||||||
| `until` | string | No | Retrieve responses submitted before this date \(e.g., "2024-12-31T23:59:59Z"\) |
|
| `until` | string | No | Retrieve responses submitted before this date \(e.g., "2024-12-31T23:59:59Z"\) |
|
||||||
| `completed` | string | No | Filter by completion status \(e.g., "true", "false", "all"\) |
|
| `completed` | string | No | Filter by completion status \(e.g., "true", "false", "all"\) |
|
||||||
|
|||||||
@@ -67,10 +67,9 @@ Retrieve a list of tickets from Zendesk with optional filtering
|
|||||||
| `type` | string | No | Filter by type: "problem", "incident", "question", or "task" |
|
| `type` | string | No | Filter by type: "problem", "incident", "question", or "task" |
|
||||||
| `assigneeId` | string | No | Filter by assignee user ID as a numeric string \(e.g., "12345"\) |
|
| `assigneeId` | string | No | Filter by assignee user ID as a numeric string \(e.g., "12345"\) |
|
||||||
| `organizationId` | string | No | Filter by organization ID as a numeric string \(e.g., "67890"\) |
|
| `organizationId` | string | No | Filter by organization ID as a numeric string \(e.g., "67890"\) |
|
||||||
| `sortBy` | string | No | Sort field: "created_at", "updated_at", "priority", or "status" |
|
| `sort` | string | No | Sort field for ticket listing \(only applies without filters\): "updated_at", "id", or "status". Prefix with "-" for descending \(e.g., "-updated_at"\) |
|
||||||
| `sortOrder` | string | No | Sort order: "asc" or "desc" |
|
|
||||||
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
||||||
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
|
| `pageAfter` | string | No | Cursor from a previous response to fetch the next page of results |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -129,10 +128,10 @@ Retrieve a list of tickets from Zendesk with optional filtering
|
|||||||
| ↳ `from_messaging_channel` | boolean | Whether the ticket originated from a messaging channel |
|
| ↳ `from_messaging_channel` | boolean | Whether the ticket originated from a messaging channel |
|
||||||
| ↳ `ticket_form_id` | number | Ticket form ID |
|
| ↳ `ticket_form_id` | number | Ticket form ID |
|
||||||
| ↳ `generated_timestamp` | number | Unix timestamp of the ticket generation |
|
| ↳ `generated_timestamp` | number | Unix timestamp of the ticket generation |
|
||||||
| `paging` | object | Pagination information |
|
| `paging` | object | Cursor-based pagination information |
|
||||||
|
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
|
||||||
|
| ↳ `has_more` | boolean | Whether more results are available |
|
||||||
| ↳ `next_page` | string | URL for next page of results |
|
| ↳ `next_page` | string | URL for next page of results |
|
||||||
| ↳ `previous_page` | string | URL for previous page of results |
|
|
||||||
| ↳ `count` | number | Total count of items |
|
|
||||||
| `metadata` | object | Response metadata |
|
| `metadata` | object | Response metadata |
|
||||||
| ↳ `total_returned` | number | Number of items returned in this response |
|
| ↳ `total_returned` | number | Number of items returned in this response |
|
||||||
| ↳ `has_more` | boolean | Whether more items are available |
|
| ↳ `has_more` | boolean | Whether more items are available |
|
||||||
@@ -515,7 +514,7 @@ Retrieve a list of users from Zendesk with optional filtering
|
|||||||
| `role` | string | No | Filter by role: "end-user", "agent", or "admin" |
|
| `role` | string | No | Filter by role: "end-user", "agent", or "admin" |
|
||||||
| `permissionSet` | string | No | Filter by permission set ID as a numeric string \(e.g., "12345"\) |
|
| `permissionSet` | string | No | Filter by permission set ID as a numeric string \(e.g., "12345"\) |
|
||||||
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
||||||
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
|
| `pageAfter` | string | No | Cursor from a previous response to fetch the next page of results |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -563,10 +562,10 @@ Retrieve a list of users from Zendesk with optional filtering
|
|||||||
| ↳ `shared` | boolean | Whether the user is shared from a different Zendesk |
|
| ↳ `shared` | boolean | Whether the user is shared from a different Zendesk |
|
||||||
| ↳ `shared_agent` | boolean | Whether the agent is shared from a different Zendesk |
|
| ↳ `shared_agent` | boolean | Whether the agent is shared from a different Zendesk |
|
||||||
| ↳ `remote_photo_url` | string | URL to a remote photo |
|
| ↳ `remote_photo_url` | string | URL to a remote photo |
|
||||||
| `paging` | object | Pagination information |
|
| `paging` | object | Cursor-based pagination information |
|
||||||
|
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
|
||||||
|
| ↳ `has_more` | boolean | Whether more results are available |
|
||||||
| ↳ `next_page` | string | URL for next page of results |
|
| ↳ `next_page` | string | URL for next page of results |
|
||||||
| ↳ `previous_page` | string | URL for previous page of results |
|
|
||||||
| ↳ `count` | number | Total count of items |
|
|
||||||
| `metadata` | object | Response metadata |
|
| `metadata` | object | Response metadata |
|
||||||
| ↳ `total_returned` | number | Number of items returned in this response |
|
| ↳ `total_returned` | number | Number of items returned in this response |
|
||||||
| ↳ `has_more` | boolean | Whether more items are available |
|
| ↳ `has_more` | boolean | Whether more items are available |
|
||||||
@@ -706,7 +705,7 @@ Search for users in Zendesk using a query string
|
|||||||
| `query` | string | No | Search query string \(e.g., user name or email\) |
|
| `query` | string | No | Search query string \(e.g., user name or email\) |
|
||||||
| `externalId` | string | No | External ID to search by \(your system identifier\) |
|
| `externalId` | string | No | External ID to search by \(your system identifier\) |
|
||||||
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
||||||
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
|
| `page` | string | No | Page number for pagination \(1-based\) |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -754,10 +753,10 @@ Search for users in Zendesk using a query string
|
|||||||
| ↳ `shared` | boolean | Whether the user is shared from a different Zendesk |
|
| ↳ `shared` | boolean | Whether the user is shared from a different Zendesk |
|
||||||
| ↳ `shared_agent` | boolean | Whether the agent is shared from a different Zendesk |
|
| ↳ `shared_agent` | boolean | Whether the agent is shared from a different Zendesk |
|
||||||
| ↳ `remote_photo_url` | string | URL to a remote photo |
|
| ↳ `remote_photo_url` | string | URL to a remote photo |
|
||||||
| `paging` | object | Pagination information |
|
| `paging` | object | Cursor-based pagination information |
|
||||||
|
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
|
||||||
|
| ↳ `has_more` | boolean | Whether more results are available |
|
||||||
| ↳ `next_page` | string | URL for next page of results |
|
| ↳ `next_page` | string | URL for next page of results |
|
||||||
| ↳ `previous_page` | string | URL for previous page of results |
|
|
||||||
| ↳ `count` | number | Total count of items |
|
|
||||||
| `metadata` | object | Response metadata |
|
| `metadata` | object | Response metadata |
|
||||||
| ↳ `total_returned` | number | Number of items returned in this response |
|
| ↳ `total_returned` | number | Number of items returned in this response |
|
||||||
| ↳ `has_more` | boolean | Whether more items are available |
|
| ↳ `has_more` | boolean | Whether more items are available |
|
||||||
@@ -999,7 +998,7 @@ Retrieve a list of organizations from Zendesk
|
|||||||
| `apiToken` | string | Yes | Zendesk API token |
|
| `apiToken` | string | Yes | Zendesk API token |
|
||||||
| `subdomain` | string | Yes | Your Zendesk subdomain \(e.g., "mycompany" for mycompany.zendesk.com\) |
|
| `subdomain` | string | Yes | Your Zendesk subdomain \(e.g., "mycompany" for mycompany.zendesk.com\) |
|
||||||
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
||||||
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
|
| `pageAfter` | string | No | Cursor from a previous response to fetch the next page of results |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -1020,10 +1019,10 @@ Retrieve a list of organizations from Zendesk
|
|||||||
| ↳ `created_at` | string | When the organization was created \(ISO 8601 format\) |
|
| ↳ `created_at` | string | When the organization was created \(ISO 8601 format\) |
|
||||||
| ↳ `updated_at` | string | When the organization was last updated \(ISO 8601 format\) |
|
| ↳ `updated_at` | string | When the organization was last updated \(ISO 8601 format\) |
|
||||||
| ↳ `external_id` | string | External ID for linking to external records |
|
| ↳ `external_id` | string | External ID for linking to external records |
|
||||||
| `paging` | object | Pagination information |
|
| `paging` | object | Cursor-based pagination information |
|
||||||
|
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
|
||||||
|
| ↳ `has_more` | boolean | Whether more results are available |
|
||||||
| ↳ `next_page` | string | URL for next page of results |
|
| ↳ `next_page` | string | URL for next page of results |
|
||||||
| ↳ `previous_page` | string | URL for previous page of results |
|
|
||||||
| ↳ `count` | number | Total count of items |
|
|
||||||
| `metadata` | object | Response metadata |
|
| `metadata` | object | Response metadata |
|
||||||
| ↳ `total_returned` | number | Number of items returned in this response |
|
| ↳ `total_returned` | number | Number of items returned in this response |
|
||||||
| ↳ `has_more` | boolean | Whether more items are available |
|
| ↳ `has_more` | boolean | Whether more items are available |
|
||||||
@@ -1075,7 +1074,7 @@ Autocomplete organizations in Zendesk by name prefix (for name matching/autocomp
|
|||||||
| `subdomain` | string | Yes | Your Zendesk subdomain |
|
| `subdomain` | string | Yes | Your Zendesk subdomain |
|
||||||
| `name` | string | Yes | Organization name prefix to search for \(e.g., "Acme"\) |
|
| `name` | string | Yes | Organization name prefix to search for \(e.g., "Acme"\) |
|
||||||
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
||||||
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
|
| `page` | string | No | Page number for pagination \(1-based\) |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -1096,10 +1095,10 @@ Autocomplete organizations in Zendesk by name prefix (for name matching/autocomp
|
|||||||
| ↳ `created_at` | string | When the organization was created \(ISO 8601 format\) |
|
| ↳ `created_at` | string | When the organization was created \(ISO 8601 format\) |
|
||||||
| ↳ `updated_at` | string | When the organization was last updated \(ISO 8601 format\) |
|
| ↳ `updated_at` | string | When the organization was last updated \(ISO 8601 format\) |
|
||||||
| ↳ `external_id` | string | External ID for linking to external records |
|
| ↳ `external_id` | string | External ID for linking to external records |
|
||||||
| `paging` | object | Pagination information |
|
| `paging` | object | Cursor-based pagination information |
|
||||||
|
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
|
||||||
|
| ↳ `has_more` | boolean | Whether more results are available |
|
||||||
| ↳ `next_page` | string | URL for next page of results |
|
| ↳ `next_page` | string | URL for next page of results |
|
||||||
| ↳ `previous_page` | string | URL for previous page of results |
|
|
||||||
| ↳ `count` | number | Total count of items |
|
|
||||||
| `metadata` | object | Response metadata |
|
| `metadata` | object | Response metadata |
|
||||||
| ↳ `total_returned` | number | Number of items returned in this response |
|
| ↳ `total_returned` | number | Number of items returned in this response |
|
||||||
| ↳ `has_more` | boolean | Whether more items are available |
|
| ↳ `has_more` | boolean | Whether more items are available |
|
||||||
@@ -1249,19 +1248,18 @@ Unified search across tickets, users, and organizations in Zendesk
|
|||||||
| `apiToken` | string | Yes | Zendesk API token |
|
| `apiToken` | string | Yes | Zendesk API token |
|
||||||
| `subdomain` | string | Yes | Your Zendesk subdomain |
|
| `subdomain` | string | Yes | Your Zendesk subdomain |
|
||||||
| `query` | string | Yes | Search query string using Zendesk search syntax \(e.g., "type:ticket status:open"\) |
|
| `query` | string | Yes | Search query string using Zendesk search syntax \(e.g., "type:ticket status:open"\) |
|
||||||
| `sortBy` | string | No | Sort field: "relevance", "created_at", "updated_at", "priority", "status", or "ticket_type" |
|
| `filterType` | string | Yes | Resource type to search for: "ticket", "user", "organization", or "group" |
|
||||||
| `sortOrder` | string | No | Sort order: "asc" or "desc" |
|
|
||||||
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
|
||||||
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
|
| `pageAfter` | string | No | Cursor from a previous response to fetch the next page of results |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `paging` | object | Pagination information |
|
| `paging` | object | Cursor-based pagination information |
|
||||||
|
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
|
||||||
|
| ↳ `has_more` | boolean | Whether more results are available |
|
||||||
| ↳ `next_page` | string | URL for next page of results |
|
| ↳ `next_page` | string | URL for next page of results |
|
||||||
| ↳ `previous_page` | string | URL for previous page of results |
|
|
||||||
| ↳ `count` | number | Total count of items |
|
|
||||||
| `metadata` | object | Response metadata |
|
| `metadata` | object | Response metadata |
|
||||||
| ↳ `total_returned` | number | Number of items returned in this response |
|
| ↳ `total_returned` | number | Number of items returned in this response |
|
||||||
| ↳ `has_more` | boolean | Whether more items are available |
|
| ↳ `has_more` | boolean | Whether more items are available |
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
'use server'
|
|
||||||
|
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { isProd } from '@/lib/core/config/feature-flags'
|
import { isProd } from '@/lib/core/config/feature-flags'
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
|||||||
transform: isAnimated ? 'translateY(0) scale(1)' : 'translateY(8px) scale(0.98)',
|
transform: isAnimated ? 'translateY(0) scale(1)' : 'translateY(8px) scale(0.98)',
|
||||||
transition:
|
transition:
|
||||||
'opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1), transform 0.6s cubic-bezier(0.22, 1, 0.36, 1)',
|
'opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1), transform 0.6s cubic-bezier(0.22, 1, 0.36, 1)',
|
||||||
willChange: 'transform, opacity',
|
willChange: isAnimated ? 'auto' : 'transform, opacity',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LandingBlock icon={data.icon} color={data.color} name={data.name} tags={data.tags} />
|
<LandingBlock icon={data.icon} color={data.color} name={data.name} tags={data.tags} />
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
|||||||
strokeLinejoin: 'round',
|
strokeLinejoin: 'round',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
animation: `landing-edge-dash-${id} 1s linear infinite`,
|
animation: `landing-edge-dash-${id} 1s linear infinite`,
|
||||||
willChange: 'stroke-dashoffset',
|
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -754,3 +754,100 @@ input[type="search"]::-ms-clear {
|
|||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respect user's prefers-reduced-motion setting (WCAG 2.3.3)
|
||||||
|
* Disables animations and transitions for users who prefer reduced motion.
|
||||||
|
*/
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WandPromptBar status indicator */
|
||||||
|
@keyframes smoke-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
position: relative;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: hsl(var(--muted-foreground) / 0.5);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.streaming {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.streaming::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
hsl(var(--primary) / 0.9) 0%,
|
||||||
|
hsl(var(--primary) / 0.4) 60%,
|
||||||
|
transparent 80%
|
||||||
|
);
|
||||||
|
animation: smoke-pulse 1.8s ease-in-out infinite;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .status-indicator.streaming::before {
|
||||||
|
background: #6b7280;
|
||||||
|
opacity: 0.9;
|
||||||
|
animation: smoke-pulse 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MessageContainer loading dot */
|
||||||
|
@keyframes growShrink {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dot {
|
||||||
|
animation: growShrink 1.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subflow node z-index and drag-over styles */
|
||||||
|
.workflow-container .react-flow__node-subflowNode {
|
||||||
|
z-index: -1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-container .react-flow__node-subflowNode:has([data-subflow-selected="true"]) {
|
||||||
|
z-index: 10 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loop-node-drag-over,
|
||||||
|
.parallel-node-drag-over {
|
||||||
|
box-shadow: 0 0 0 1.75px var(--brand-secondary) !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node[data-parent-node-id] .react-flow__handle {
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import { createMockLogger, createMockRequest } from '@sim/testing'
|
import { auditMock, createMockLogger, createMockRequest } from '@sim/testing'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
describe('OAuth Disconnect API Route', () => {
|
describe('OAuth Disconnect API Route', () => {
|
||||||
@@ -67,6 +67,8 @@ describe('OAuth Disconnect API Route', () => {
|
|||||||
vi.doMock('@/lib/webhooks/utils.server', () => ({
|
vi.doMock('@/lib/webhooks/utils.server', () => ({
|
||||||
syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet,
|
syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.doMock('@/lib/audit/log', () => auditMock)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq, like, or } from 'drizzle-orm'
|
import { and, eq, like, 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||||
@@ -118,6 +119,20 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.OAUTH_DISCONNECTED,
|
||||||
|
resourceType: AuditResourceType.OAUTH,
|
||||||
|
resourceId: providerId ?? provider,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: provider,
|
||||||
|
description: `Disconnected OAuth provider: ${provider}`,
|
||||||
|
metadata: { provider, providerId },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 })
|
return NextResponse.json({ success: true }, { status: 200 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error disconnecting OAuth provider`, error)
|
logger.error(`[${requestId}] Error disconnecting OAuth provider`, error)
|
||||||
|
|||||||
@@ -1,6 +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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { getCreditBalance } from '@/lib/billing/credits/balance'
|
import { getCreditBalance } from '@/lib/billing/credits/balance'
|
||||||
import { purchaseCredits } from '@/lib/billing/credits/purchase'
|
import { purchaseCredits } from '@/lib/billing/credits/purchase'
|
||||||
@@ -57,6 +58,17 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.CREDIT_PURCHASED,
|
||||||
|
resourceType: AuditResourceType.BILLING,
|
||||||
|
description: `Purchased $${validation.data.amount} in credits`,
|
||||||
|
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to purchase credits', { error, userId: session.user.id })
|
logger.error('Failed to purchase credits', { error, userId: session.user.id })
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
*
|
*
|
||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import { loggerMock } from '@sim/testing'
|
import { auditMock, loggerMock } from '@sim/testing'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
vi.mock('@/lib/core/config/feature-flags', () => ({
|
vi.mock('@/lib/core/config/feature-flags', () => ({
|
||||||
isDev: true,
|
isDev: true,
|
||||||
isHosted: false,
|
isHosted: false,
|
||||||
@@ -216,8 +218,11 @@ describe('Chat Edit API Route', () => {
|
|||||||
workflowId: 'workflow-123',
|
workflowId: 'workflow-123',
|
||||||
}
|
}
|
||||||
|
|
||||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
|
mockCheckChatAccess.mockResolvedValue({
|
||||||
mockLimit.mockResolvedValueOnce([]) // No identifier conflict
|
hasAccess: true,
|
||||||
|
chat: mockChat,
|
||||||
|
workspaceId: 'workspace-123',
|
||||||
|
})
|
||||||
|
|
||||||
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
|
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -311,8 +316,11 @@ describe('Chat Edit API Route', () => {
|
|||||||
workflowId: 'workflow-123',
|
workflowId: 'workflow-123',
|
||||||
}
|
}
|
||||||
|
|
||||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
|
mockCheckChatAccess.mockResolvedValue({
|
||||||
mockLimit.mockResolvedValueOnce([])
|
hasAccess: true,
|
||||||
|
chat: mockChat,
|
||||||
|
workspaceId: 'workspace-123',
|
||||||
|
})
|
||||||
|
|
||||||
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
|
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -371,8 +379,11 @@ describe('Chat Edit API Route', () => {
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
|
mockCheckChatAccess.mockResolvedValue({
|
||||||
mockWhere.mockResolvedValue(undefined)
|
hasAccess: true,
|
||||||
|
chat: { title: 'Test Chat', workflowId: 'workflow-123' },
|
||||||
|
workspaceId: 'workspace-123',
|
||||||
|
})
|
||||||
|
|
||||||
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
|
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -393,8 +404,11 @@ describe('Chat Edit API Route', () => {
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
|
mockCheckChatAccess.mockResolvedValue({
|
||||||
mockWhere.mockResolvedValue(undefined)
|
hasAccess: true,
|
||||||
|
chat: { title: 'Test Chat', workflowId: 'workflow-123' },
|
||||||
|
workspaceId: 'workspace-123',
|
||||||
|
})
|
||||||
|
|
||||||
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
|
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { isDev } from '@/lib/core/config/feature-flags'
|
import { isDev } from '@/lib/core/config/feature-flags'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
@@ -103,7 +104,11 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
try {
|
try {
|
||||||
const validatedData = chatUpdateSchema.parse(body)
|
const validatedData = chatUpdateSchema.parse(body)
|
||||||
|
|
||||||
const { hasAccess, chat: existingChatRecord } = await checkChatAccess(chatId, session.user.id)
|
const {
|
||||||
|
hasAccess,
|
||||||
|
chat: existingChatRecord,
|
||||||
|
workspaceId: chatWorkspaceId,
|
||||||
|
} = await checkChatAccess(chatId, session.user.id)
|
||||||
|
|
||||||
if (!hasAccess || !existingChatRecord) {
|
if (!hasAccess || !existingChatRecord) {
|
||||||
return createErrorResponse('Chat not found or access denied', 404)
|
return createErrorResponse('Chat not found or access denied', 404)
|
||||||
@@ -217,6 +222,19 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
|
|
||||||
logger.info(`Chat "${chatId}" updated successfully`)
|
logger.info(`Chat "${chatId}" updated successfully`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: chatWorkspaceId || null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.CHAT_UPDATED,
|
||||||
|
resourceType: AuditResourceType.CHAT,
|
||||||
|
resourceId: chatId,
|
||||||
|
resourceName: title || existingChatRecord.title,
|
||||||
|
description: `Updated chat deployment "${title || existingChatRecord.title}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
id: chatId,
|
id: chatId,
|
||||||
chatUrl,
|
chatUrl,
|
||||||
@@ -252,7 +270,11 @@ export async function DELETE(
|
|||||||
return createErrorResponse('Unauthorized', 401)
|
return createErrorResponse('Unauthorized', 401)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasAccess } = await checkChatAccess(chatId, session.user.id)
|
const {
|
||||||
|
hasAccess,
|
||||||
|
chat: chatRecord,
|
||||||
|
workspaceId: chatWorkspaceId,
|
||||||
|
} = await checkChatAccess(chatId, session.user.id)
|
||||||
|
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
return createErrorResponse('Chat not found or access denied', 404)
|
return createErrorResponse('Chat not found or access denied', 404)
|
||||||
@@ -262,6 +284,19 @@ export async function DELETE(
|
|||||||
|
|
||||||
logger.info(`Chat "${chatId}" deleted successfully`)
|
logger.info(`Chat "${chatId}" deleted successfully`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: chatWorkspaceId || null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.CHAT_DELETED,
|
||||||
|
resourceType: AuditResourceType.CHAT,
|
||||||
|
resourceId: chatId,
|
||||||
|
resourceName: chatRecord?.title || chatId,
|
||||||
|
description: `Deleted chat deployment "${chatRecord?.title || chatId}"`,
|
||||||
|
request: _request,
|
||||||
|
})
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
message: 'Chat deployment deleted successfully',
|
message: 'Chat deployment deleted successfully',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { NextRequest } from 'next/server'
|
|
||||||
/**
|
/**
|
||||||
* Tests for chat API route
|
* Tests for chat API route
|
||||||
*
|
*
|
||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
|
import { auditMock } from '@sim/testing'
|
||||||
|
import { NextRequest } from 'next/server'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
describe('Chat API Route', () => {
|
describe('Chat API Route', () => {
|
||||||
@@ -30,6 +31,8 @@ describe('Chat API Route', () => {
|
|||||||
mockInsert.mockReturnValue({ values: mockValues })
|
mockInsert.mockReturnValue({ values: mockValues })
|
||||||
mockValues.mockReturnValue({ returning: mockReturning })
|
mockValues.mockReturnValue({ returning: mockReturning })
|
||||||
|
|
||||||
|
vi.doMock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
vi.doMock('@sim/db', () => ({
|
vi.doMock('@sim/db', () => ({
|
||||||
db: {
|
db: {
|
||||||
select: mockSelect,
|
select: mockSelect,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm'
|
|||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { isDev } from '@/lib/core/config/feature-flags'
|
import { isDev } from '@/lib/core/config/feature-flags'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
@@ -42,7 +43,7 @@ const chatSchema = z.object({
|
|||||||
.default([]),
|
.default([]),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(_request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await getSession()
|
const session = await getSession()
|
||||||
|
|
||||||
@@ -174,7 +175,7 @@ export async function POST(request: NextRequest) {
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
identifier,
|
identifier,
|
||||||
title,
|
title,
|
||||||
description: description || '',
|
description: description || null,
|
||||||
customizations: mergedCustomizations,
|
customizations: mergedCustomizations,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
authType,
|
authType,
|
||||||
@@ -224,6 +225,20 @@ export async function POST(request: NextRequest) {
|
|||||||
// Silently fail
|
// Silently fail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workflowRecord.workspaceId || null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.CHAT_DEPLOYED,
|
||||||
|
resourceType: AuditResourceType.CHAT,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: title,
|
||||||
|
description: `Deployed chat "${title}"`,
|
||||||
|
metadata: { workflowId, identifier, authType },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
id,
|
id,
|
||||||
chatUrl,
|
chatUrl,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export async function checkWorkflowAccessForChatCreation(
|
|||||||
export async function checkChatAccess(
|
export async function checkChatAccess(
|
||||||
chatId: string,
|
chatId: string,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ hasAccess: boolean; chat?: any }> {
|
): Promise<{ hasAccess: boolean; chat?: any; workspaceId?: string }> {
|
||||||
const chatData = await db
|
const chatData = await db
|
||||||
.select({
|
.select({
|
||||||
chat: chat,
|
chat: chat,
|
||||||
@@ -78,7 +78,9 @@ export async function checkChatAccess(
|
|||||||
action: 'admin',
|
action: 'admin',
|
||||||
})
|
})
|
||||||
|
|
||||||
return authorization.allowed ? { hasAccess: true, chat: chatRecord } : { hasAccess: false }
|
return authorization.allowed
|
||||||
|
? { hasAccess: true, chat: chatRecord, workspaceId: workflowWorkspaceId }
|
||||||
|
: { hasAccess: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateChatAuth(
|
export async function validateChatAuth(
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
|
import { db } from '@sim/db'
|
||||||
|
import { settings } from '@sim/db/schema'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
|
||||||
import { env } from '@/lib/core/config/env'
|
|
||||||
|
|
||||||
const logger = createLogger('CopilotAutoAllowedToolsAPI')
|
const logger = createLogger('CopilotAutoAllowedToolsAPI')
|
||||||
|
|
||||||
/** Headers for server-to-server calls to the Go copilot backend. */
|
|
||||||
function copilotHeaders(): Record<string, string> {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
if (env.COPILOT_API_KEY) {
|
|
||||||
headers['x-api-key'] = env.COPILOT_API_KEY
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET - Fetch user's auto-allowed integration tools
|
* GET - Fetch user's auto-allowed integration tools
|
||||||
*/
|
*/
|
||||||
@@ -30,18 +20,24 @@ export async function GET() {
|
|||||||
|
|
||||||
const userId = session.user.id
|
const userId = session.user.id
|
||||||
|
|
||||||
const res = await fetch(
|
const [userSettings] = await db
|
||||||
`${SIM_AGENT_API_URL}/api/tool-preferences/auto-allowed?userId=${encodeURIComponent(userId)}`,
|
.select()
|
||||||
{ method: 'GET', headers: copilotHeaders() }
|
.from(settings)
|
||||||
)
|
.where(eq(settings.userId, userId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
if (!res.ok) {
|
if (userSettings) {
|
||||||
logger.warn('Go backend returned error for list auto-allowed', { status: res.status })
|
const autoAllowedTools = (userSettings.copilotAutoAllowedTools as string[]) || []
|
||||||
return NextResponse.json({ autoAllowedTools: [] })
|
return NextResponse.json({ autoAllowedTools })
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await res.json()
|
await db.insert(settings).values({
|
||||||
return NextResponse.json({ autoAllowedTools: payload?.autoAllowedTools || [] })
|
id: userId,
|
||||||
|
userId,
|
||||||
|
copilotAutoAllowedTools: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ autoAllowedTools: [] })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch auto-allowed tools', { error })
|
logger.error('Failed to fetch auto-allowed tools', { error })
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||||
@@ -66,22 +62,38 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'toolId must be a string' }, { status: 400 })
|
return NextResponse.json({ error: 'toolId must be a string' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${SIM_AGENT_API_URL}/api/tool-preferences/auto-allowed`, {
|
const toolId = body.toolId
|
||||||
method: 'POST',
|
|
||||||
headers: copilotHeaders(),
|
|
||||||
body: JSON.stringify({ userId, toolId: body.toolId }),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) {
|
const [existing] = await db.select().from(settings).where(eq(settings.userId, userId)).limit(1)
|
||||||
logger.warn('Go backend returned error for add auto-allowed', { status: res.status })
|
|
||||||
return NextResponse.json({ error: 'Failed to add tool' }, { status: 500 })
|
if (existing) {
|
||||||
|
const currentTools = (existing.copilotAutoAllowedTools as string[]) || []
|
||||||
|
|
||||||
|
if (!currentTools.includes(toolId)) {
|
||||||
|
const updatedTools = [...currentTools, toolId]
|
||||||
|
await db
|
||||||
|
.update(settings)
|
||||||
|
.set({
|
||||||
|
copilotAutoAllowedTools: updatedTools,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(settings.userId, userId))
|
||||||
|
|
||||||
|
logger.info('Added tool to auto-allowed list', { userId, toolId })
|
||||||
|
return NextResponse.json({ success: true, autoAllowedTools: updatedTools })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, autoAllowedTools: currentTools })
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await res.json()
|
await db.insert(settings).values({
|
||||||
return NextResponse.json({
|
id: userId,
|
||||||
success: true,
|
userId,
|
||||||
autoAllowedTools: payload?.autoAllowedTools || [],
|
copilotAutoAllowedTools: [toolId],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logger.info('Created settings and added tool to auto-allowed list', { userId, toolId })
|
||||||
|
return NextResponse.json({ success: true, autoAllowedTools: [toolId] })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to add auto-allowed tool', { error })
|
logger.error('Failed to add auto-allowed tool', { error })
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||||
@@ -107,21 +119,25 @@ export async function DELETE(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'toolId query parameter is required' }, { status: 400 })
|
return NextResponse.json({ error: 'toolId query parameter is required' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(
|
const [existing] = await db.select().from(settings).where(eq(settings.userId, userId)).limit(1)
|
||||||
`${SIM_AGENT_API_URL}/api/tool-preferences/auto-allowed?userId=${encodeURIComponent(userId)}&toolId=${encodeURIComponent(toolId)}`,
|
|
||||||
{ method: 'DELETE', headers: copilotHeaders() }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!res.ok) {
|
if (existing) {
|
||||||
logger.warn('Go backend returned error for remove auto-allowed', { status: res.status })
|
const currentTools = (existing.copilotAutoAllowedTools as string[]) || []
|
||||||
return NextResponse.json({ error: 'Failed to remove tool' }, { status: 500 })
|
const updatedTools = currentTools.filter((t) => t !== toolId)
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(settings)
|
||||||
|
.set({
|
||||||
|
copilotAutoAllowedTools: updatedTools,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(settings.userId, userId))
|
||||||
|
|
||||||
|
logger.info('Removed tool from auto-allowed list', { userId, toolId })
|
||||||
|
return NextResponse.json({ success: true, autoAllowedTools: updatedTools })
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await res.json()
|
return NextResponse.json({ success: true, autoAllowedTools: [] })
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
autoAllowedTools: payload?.autoAllowedTools || [],
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to remove auto-allowed tool', { error })
|
logger.error('Failed to remove auto-allowed tool', { error })
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
import { db } from '@sim/db'
|
|
||||||
import { copilotChats } from '@sim/db/schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { eq } from 'drizzle-orm'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { getSession } from '@/lib/auth'
|
|
||||||
import { resolveOrCreateChat } from '@/lib/copilot/chat-lifecycle'
|
|
||||||
import { SIM_AGENT_VERSION } from '@/lib/copilot/constants'
|
|
||||||
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
|
|
||||||
import type { SSEEvent } from '@/lib/copilot/orchestrator/types'
|
|
||||||
// Workspace prompt is now generated by the Go copilot backend (detected via source: 'workspace-chat')
|
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceChatAPI')
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
export const runtime = 'nodejs'
|
|
||||||
export const maxDuration = 300
|
|
||||||
|
|
||||||
const WorkspaceChatSchema = z.object({
|
|
||||||
message: z.string().min(1, 'Message is required'),
|
|
||||||
workspaceId: z.string().min(1, 'workspaceId is required'),
|
|
||||||
chatId: z.string().optional(),
|
|
||||||
model: z.string().optional().default('claude-opus-4-5'),
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
|
||||||
try {
|
|
||||||
const session = await getSession()
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await req.json()
|
|
||||||
const { message, workspaceId, chatId, model } = WorkspaceChatSchema.parse(body)
|
|
||||||
|
|
||||||
const chatResult = await resolveOrCreateChat({
|
|
||||||
chatId,
|
|
||||||
userId: session.user.id,
|
|
||||||
workspaceId,
|
|
||||||
model,
|
|
||||||
})
|
|
||||||
|
|
||||||
const requestPayload: Record<string, unknown> = {
|
|
||||||
message,
|
|
||||||
userId: session.user.id,
|
|
||||||
model,
|
|
||||||
mode: 'agent',
|
|
||||||
headless: true,
|
|
||||||
messageId: crypto.randomUUID(),
|
|
||||||
version: SIM_AGENT_VERSION,
|
|
||||||
source: 'workspace-chat',
|
|
||||||
stream: true,
|
|
||||||
...(chatResult.chatId ? { chatId: chatResult.chatId } : {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
const encoder = new TextEncoder()
|
|
||||||
const stream = new ReadableStream({
|
|
||||||
async start(controller) {
|
|
||||||
const pushEvent = (event: Record<string, unknown>) => {
|
|
||||||
try {
|
|
||||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
|
|
||||||
} catch {
|
|
||||||
// Client disconnected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chatResult.chatId) {
|
|
||||||
pushEvent({ type: 'chat_id', chatId: chatResult.chatId })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await orchestrateCopilotStream(requestPayload, {
|
|
||||||
userId: session.user.id,
|
|
||||||
workspaceId,
|
|
||||||
chatId: chatResult.chatId || undefined,
|
|
||||||
autoExecuteTools: true,
|
|
||||||
interactive: false,
|
|
||||||
onEvent: async (event: SSEEvent) => {
|
|
||||||
pushEvent(event as unknown as Record<string, unknown>)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (chatResult.chatId && result.conversationId) {
|
|
||||||
await db
|
|
||||||
.update(copilotChats)
|
|
||||||
.set({
|
|
||||||
updatedAt: new Date(),
|
|
||||||
conversationId: result.conversationId,
|
|
||||||
})
|
|
||||||
.where(eq(copilotChats.id, chatResult.chatId))
|
|
||||||
}
|
|
||||||
|
|
||||||
pushEvent({
|
|
||||||
type: 'done',
|
|
||||||
success: result.success,
|
|
||||||
content: result.content,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Workspace chat orchestration failed', { error })
|
|
||||||
pushEvent({
|
|
||||||
type: 'error',
|
|
||||||
error: error instanceof Error ? error.message : 'Chat failed',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
controller.close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Response(stream, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
Connection: 'keep-alive',
|
|
||||||
'X-Accel-Buffering': 'no',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid request', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('Workspace chat error', { error })
|
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
|
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasCredentialSetsAccess } from '@/lib/billing'
|
import { hasCredentialSetsAccess } from '@/lib/billing'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
@@ -148,6 +149,19 @@ export async function POST(
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.CREDENTIAL_SET_INVITATION_RESENT,
|
||||||
|
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: result.set.name,
|
||||||
|
description: `Resent credential set invitation to ${invitation.email}`,
|
||||||
|
metadata: { invitationId, email: invitation.email },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error resending invitation', error)
|
logger.error('Error resending invitation', error)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { and, eq } 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 { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
|
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasCredentialSetsAccess } from '@/lib/billing'
|
import { hasCredentialSetsAccess } from '@/lib/billing'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
@@ -175,6 +176,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
emailSent: !!email,
|
emailSent: !!email,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.CREDENTIAL_SET_INVITATION_CREATED,
|
||||||
|
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: result.set.name,
|
||||||
|
description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
invitation: {
|
invitation: {
|
||||||
...invitation,
|
...invitation,
|
||||||
@@ -235,6 +249,19 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.CREDENTIAL_SET_INVITATION_REVOKED,
|
||||||
|
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: result.set.name,
|
||||||
|
description: `Revoked invitation "${invitationId}" for credential set "${result.set.name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error cancelling invitation', error)
|
logger.error('Error cancelling invitation', error)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { account, credentialSet, credentialSetMember, member, user } from '@sim/
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, inArray } from 'drizzle-orm'
|
import { and, eq, inArray } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasCredentialSetsAccess } from '@/lib/billing'
|
import { hasCredentialSetsAccess } from '@/lib/billing'
|
||||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||||
@@ -13,6 +14,7 @@ async function getCredentialSetWithAccess(credentialSetId: string, userId: strin
|
|||||||
const [set] = await db
|
const [set] = await db
|
||||||
.select({
|
.select({
|
||||||
id: credentialSet.id,
|
id: credentialSet.id,
|
||||||
|
name: credentialSet.name,
|
||||||
organizationId: credentialSet.organizationId,
|
organizationId: credentialSet.organizationId,
|
||||||
providerId: credentialSet.providerId,
|
providerId: credentialSet.providerId,
|
||||||
})
|
})
|
||||||
@@ -177,6 +179,19 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.CREDENTIAL_SET_MEMBER_REMOVED,
|
||||||
|
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: result.set.name,
|
||||||
|
description: `Removed member from credential set "${result.set.name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error removing member from credential set', error)
|
logger.error('Error removing member from credential set', error)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasCredentialSetsAccess } from '@/lib/billing'
|
import { hasCredentialSetsAccess } from '@/lib/billing'
|
||||||
|
|
||||||
@@ -131,6 +132,19 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
|
|
||||||
const [updated] = await db.select().from(credentialSet).where(eq(credentialSet.id, id)).limit(1)
|
const [updated] = await db.select().from(credentialSet).where(eq(credentialSet.id, id)).limit(1)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.CREDENTIAL_SET_UPDATED,
|
||||||
|
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: updated?.name ?? result.set.name,
|
||||||
|
description: `Updated credential set "${updated?.name ?? result.set.name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ credentialSet: updated })
|
return NextResponse.json({ credentialSet: updated })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
@@ -175,6 +189,19 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
|||||||
|
|
||||||
logger.info('Deleted credential set', { credentialSetId: id, userId: session.user.id })
|
logger.info('Deleted credential set', { credentialSetId: id, userId: session.user.id })
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.CREDENTIAL_SET_DELETED,
|
||||||
|
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: result.set.name,
|
||||||
|
description: `Deleted credential set "${result.set.name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error deleting credential set', error)
|
logger.error('Error deleting credential set', error)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
|||||||
status: credentialSetInvitation.status,
|
status: credentialSetInvitation.status,
|
||||||
expiresAt: credentialSetInvitation.expiresAt,
|
expiresAt: credentialSetInvitation.expiresAt,
|
||||||
invitedBy: credentialSetInvitation.invitedBy,
|
invitedBy: credentialSetInvitation.invitedBy,
|
||||||
|
credentialSetName: credentialSet.name,
|
||||||
providerId: credentialSet.providerId,
|
providerId: credentialSet.providerId,
|
||||||
})
|
})
|
||||||
.from(credentialSetInvitation)
|
.from(credentialSetInvitation)
|
||||||
@@ -125,7 +127,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const requestId = crypto.randomUUID().slice(0, 8)
|
const requestId = crypto.randomUUID().slice(0, 8)
|
||||||
|
|
||||||
// Use transaction to ensure membership + invitation update + webhook sync are atomic
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
await tx.insert(credentialSetMember).values({
|
await tx.insert(credentialSetMember).values({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
@@ -147,8 +148,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
|||||||
})
|
})
|
||||||
.where(eq(credentialSetInvitation.id, invitation.id))
|
.where(eq(credentialSetInvitation.id, invitation.id))
|
||||||
|
|
||||||
// Clean up all other pending invitations for the same credential set and email
|
|
||||||
// This prevents duplicate invites from showing up after accepting one
|
|
||||||
if (invitation.email) {
|
if (invitation.email) {
|
||||||
await tx
|
await tx
|
||||||
.update(credentialSetInvitation)
|
.update(credentialSetInvitation)
|
||||||
@@ -166,7 +165,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync webhooks within the transaction
|
|
||||||
const syncResult = await syncAllWebhooksForCredentialSet(
|
const syncResult = await syncAllWebhooksForCredentialSet(
|
||||||
invitation.credentialSetId,
|
invitation.credentialSetId,
|
||||||
requestId,
|
requestId,
|
||||||
@@ -184,6 +182,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.CREDENTIAL_SET_INVITATION_ACCEPTED,
|
||||||
|
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||||
|
resourceId: invitation.credentialSetId,
|
||||||
|
resourceName: invitation.credentialSetName,
|
||||||
|
description: `Accepted credential set invitation`,
|
||||||
|
metadata: { invitationId: invitation.id },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
credentialSetId: invitation.credentialSetId,
|
credentialSetId: invitation.credentialSetId,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { credentialSet, credentialSetMember, organization } from '@sim/db/schema
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||||
|
|
||||||
@@ -106,6 +107,17 @@ export async function DELETE(req: NextRequest) {
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.CREDENTIAL_SET_MEMBER_LEFT,
|
||||||
|
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||||
|
resourceId: credentialSetId,
|
||||||
|
description: `Left credential set`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Failed to leave credential set'
|
const message = error instanceof Error ? error.message : 'Failed to leave credential set'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, count, desc, eq } from 'drizzle-orm'
|
import { and, count, desc, eq } from 'drizzle-orm'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasCredentialSetsAccess } from '@/lib/billing'
|
import { hasCredentialSetsAccess } from '@/lib/billing'
|
||||||
|
|
||||||
@@ -165,6 +166,19 @@ export async function POST(req: Request) {
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.CREDENTIAL_SET_CREATED,
|
||||||
|
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||||
|
resourceId: newCredentialSet.id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: name,
|
||||||
|
description: `Created credential set "${name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ credentialSet: newCredentialSet }, { status: 201 })
|
return NextResponse.json({ credentialSet: newCredentialSet }, { status: 201 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
@@ -53,6 +54,17 @@ export async function POST(req: NextRequest) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.ENVIRONMENT_UPDATED,
|
||||||
|
resourceType: AuditResourceType.ENVIRONMENT,
|
||||||
|
description: 'Updated global environment variables',
|
||||||
|
metadata: { variableCount: Object.keys(variables).length },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (validationError) {
|
} catch (validationError) {
|
||||||
if (validationError instanceof z.ZodError) {
|
if (validationError instanceof z.ZodError) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate'
|
import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate'
|
||||||
@@ -115,6 +116,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: targetWorkspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.FOLDER_DUPLICATED,
|
||||||
|
resourceType: AuditResourceType.FOLDER,
|
||||||
|
resourceId: newFolderId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: name,
|
||||||
|
description: `Duplicated folder "${sourceFolder.name}" as "${name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
id: newFolderId,
|
id: newFolderId,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
auditMock,
|
||||||
createMockRequest,
|
createMockRequest,
|
||||||
type MockUser,
|
type MockUser,
|
||||||
mockAuth,
|
mockAuth,
|
||||||
@@ -12,6 +13,8 @@ import {
|
|||||||
} from '@sim/testing'
|
} from '@sim/testing'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
/** Type for captured folder values in tests */
|
/** Type for captured folder values in tests */
|
||||||
interface CapturedFolderValues {
|
interface CapturedFolderValues {
|
||||||
name?: string
|
name?: string
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
@@ -167,6 +168,19 @@ export async function DELETE(
|
|||||||
deletionStats,
|
deletionStats,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: existingFolder.workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.FOLDER_DELETED,
|
||||||
|
resourceType: AuditResourceType.FOLDER,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: existingFolder.name,
|
||||||
|
description: `Deleted folder "${existingFolder.name}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
deletedItems: deletionStats,
|
deletedItems: deletionStats,
|
||||||
|
|||||||
@@ -3,9 +3,17 @@
|
|||||||
*
|
*
|
||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import { createMockRequest, mockAuth, mockConsoleLogger, setupCommonApiMocks } from '@sim/testing'
|
import {
|
||||||
|
auditMock,
|
||||||
|
createMockRequest,
|
||||||
|
mockAuth,
|
||||||
|
mockConsoleLogger,
|
||||||
|
setupCommonApiMocks,
|
||||||
|
} from '@sim/testing'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
interface CapturedFolderValues {
|
interface CapturedFolderValues {
|
||||||
name?: string
|
name?: string
|
||||||
color?: string
|
color?: string
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { workflowFolder } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, asc, desc, eq, isNull } from 'drizzle-orm'
|
import { and, asc, desc, eq, isNull } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
@@ -119,6 +120,20 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
logger.info('Created new folder:', { id, name, workspaceId, parentId })
|
logger.info('Created new folder:', { id, name, workspaceId, parentId })
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.FOLDER_CREATED,
|
||||||
|
resourceType: AuditResourceType.FOLDER,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: name.trim(),
|
||||||
|
description: `Created folder "${name.trim()}"`,
|
||||||
|
metadata: { name: name.trim() },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ folder: newFolder })
|
return NextResponse.json({ folder: newFolder })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error creating folder:', { error })
|
logger.error('Error creating folder:', { error })
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
import { checkFormAccess, DEFAULT_FORM_CUSTOMIZATIONS } from '@/app/api/form/utils'
|
import { checkFormAccess, DEFAULT_FORM_CUSTOMIZATIONS } from '@/app/api/form/utils'
|
||||||
@@ -102,7 +103,11 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
|
|
||||||
const { hasAccess, form: formRecord } = await checkFormAccess(id, session.user.id)
|
const {
|
||||||
|
hasAccess,
|
||||||
|
form: formRecord,
|
||||||
|
workspaceId: formWorkspaceId,
|
||||||
|
} = await checkFormAccess(id, session.user.id)
|
||||||
|
|
||||||
if (!hasAccess || !formRecord) {
|
if (!hasAccess || !formRecord) {
|
||||||
return createErrorResponse('Form not found or access denied', 404)
|
return createErrorResponse('Form not found or access denied', 404)
|
||||||
@@ -184,6 +189,19 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
|
|
||||||
logger.info(`Form ${id} updated successfully`)
|
logger.info(`Form ${id} updated successfully`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: formWorkspaceId ?? null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.FORM_UPDATED,
|
||||||
|
resourceType: AuditResourceType.FORM,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: formRecord.title ?? undefined,
|
||||||
|
description: `Updated form "${formRecord.title}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
message: 'Form updated successfully',
|
message: 'Form updated successfully',
|
||||||
})
|
})
|
||||||
@@ -213,7 +231,11 @@ export async function DELETE(
|
|||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
|
|
||||||
const { hasAccess, form: formRecord } = await checkFormAccess(id, session.user.id)
|
const {
|
||||||
|
hasAccess,
|
||||||
|
form: formRecord,
|
||||||
|
workspaceId: formWorkspaceId,
|
||||||
|
} = await checkFormAccess(id, session.user.id)
|
||||||
|
|
||||||
if (!hasAccess || !formRecord) {
|
if (!hasAccess || !formRecord) {
|
||||||
return createErrorResponse('Form not found or access denied', 404)
|
return createErrorResponse('Form not found or access denied', 404)
|
||||||
@@ -223,6 +245,19 @@ export async function DELETE(
|
|||||||
|
|
||||||
logger.info(`Form ${id} deleted (soft delete)`)
|
logger.info(`Form ${id} deleted (soft delete)`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: formWorkspaceId ?? null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.FORM_DELETED,
|
||||||
|
resourceType: AuditResourceType.FORM,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: formRecord.title ?? undefined,
|
||||||
|
description: `Deleted form "${formRecord.title}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
message: 'Form deleted successfully',
|
message: 'Form deleted successfully',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm'
|
|||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { isDev } from '@/lib/core/config/feature-flags'
|
import { isDev } from '@/lib/core/config/feature-flags'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
@@ -178,7 +179,7 @@ export async function POST(request: NextRequest) {
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
identifier,
|
identifier,
|
||||||
title,
|
title,
|
||||||
description: description || '',
|
description: description || null,
|
||||||
customizations: mergedCustomizations,
|
customizations: mergedCustomizations,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
authType,
|
authType,
|
||||||
@@ -195,6 +196,19 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
logger.info(`Form "${title}" deployed successfully at ${formUrl}`)
|
logger.info(`Form "${title}" deployed successfully at ${formUrl}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workflowRecord.workspaceId ?? null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.FORM_CREATED,
|
||||||
|
resourceType: AuditResourceType.FORM,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: title,
|
||||||
|
description: `Created form "${title}" for workflow ${workflowId}`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
id,
|
id,
|
||||||
formUrl,
|
formUrl,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export async function checkWorkflowAccessForFormCreation(
|
|||||||
export async function checkFormAccess(
|
export async function checkFormAccess(
|
||||||
formId: string,
|
formId: string,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ hasAccess: boolean; form?: any }> {
|
): Promise<{ hasAccess: boolean; form?: any; workspaceId?: string }> {
|
||||||
const formData = await db
|
const formData = await db
|
||||||
.select({ form: form, workflowWorkspaceId: workflow.workspaceId })
|
.select({ form: form, workflowWorkspaceId: workflow.workspaceId })
|
||||||
.from(form)
|
.from(form)
|
||||||
@@ -75,7 +75,9 @@ export async function checkFormAccess(
|
|||||||
action: 'admin',
|
action: 'admin',
|
||||||
})
|
})
|
||||||
|
|
||||||
return authorization.allowed ? { hasAccess: true, form: formRecord } : { hasAccess: false }
|
return authorization.allowed
|
||||||
|
? { hasAccess: true, form: formRecord, workspaceId: workflowWorkspaceId }
|
||||||
|
: { hasAccess: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateFormAuth(
|
export async function validateFormAuth(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
auditMock,
|
||||||
createMockRequest,
|
createMockRequest,
|
||||||
mockAuth,
|
mockAuth,
|
||||||
mockConsoleLogger,
|
mockConsoleLogger,
|
||||||
@@ -35,6 +36,8 @@ vi.mock('@/lib/knowledge/documents/service', () => ({
|
|||||||
mockDrizzleOrm()
|
mockDrizzleOrm()
|
||||||
mockConsoleLogger()
|
mockConsoleLogger()
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
describe('Document By ID API Route', () => {
|
describe('Document By ID API Route', () => {
|
||||||
const mockAuth$ = mockAuth()
|
const mockAuth$ = mockAuth()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } 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 {
|
import {
|
||||||
@@ -197,6 +198,19 @@ export async function PUT(
|
|||||||
`[${requestId}] Document updated: ${documentId} in knowledge base ${knowledgeBaseId}`
|
`[${requestId}] Document updated: ${documentId} in knowledge base ${knowledgeBaseId}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.DOCUMENT_UPDATED,
|
||||||
|
resourceType: AuditResourceType.DOCUMENT,
|
||||||
|
resourceId: documentId,
|
||||||
|
resourceName: validatedData.filename ?? accessCheck.document?.filename,
|
||||||
|
description: `Updated document "${documentId}" in knowledge base "${knowledgeBaseId}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: updatedDocument,
|
data: updatedDocument,
|
||||||
@@ -257,6 +271,19 @@ export async function DELETE(
|
|||||||
`[${requestId}] Document deleted: ${documentId} from knowledge base ${knowledgeBaseId}`
|
`[${requestId}] Document deleted: ${documentId} from knowledge base ${knowledgeBaseId}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.DOCUMENT_DELETED,
|
||||||
|
resourceType: AuditResourceType.DOCUMENT,
|
||||||
|
resourceId: documentId,
|
||||||
|
resourceName: accessCheck.document?.filename,
|
||||||
|
description: `Deleted document "${documentId}" from knowledge base "${knowledgeBaseId}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: result,
|
data: result,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
auditMock,
|
||||||
createMockRequest,
|
createMockRequest,
|
||||||
mockAuth,
|
mockAuth,
|
||||||
mockConsoleLogger,
|
mockConsoleLogger,
|
||||||
@@ -40,6 +41,8 @@ vi.mock('@/lib/knowledge/documents/service', () => ({
|
|||||||
mockDrizzleOrm()
|
mockDrizzleOrm()
|
||||||
mockConsoleLogger()
|
mockConsoleLogger()
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
describe('Knowledge Base Documents API Route', () => {
|
describe('Knowledge Base Documents API Route', () => {
|
||||||
const mockAuth$ = mockAuth()
|
const mockAuth$ = mockAuth()
|
||||||
|
|
||||||
|
|||||||
@@ -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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import {
|
import {
|
||||||
@@ -244,6 +245,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
logger.error(`[${requestId}] Critical error in document processing pipeline:`, error)
|
logger.error(`[${requestId}] Critical error in document processing pipeline:`, error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.DOCUMENT_UPLOADED,
|
||||||
|
resourceType: AuditResourceType.DOCUMENT,
|
||||||
|
resourceId: knowledgeBaseId,
|
||||||
|
resourceName: `${createdDocuments.length} document(s)`,
|
||||||
|
description: `Uploaded ${createdDocuments.length} document(s) to knowledge base "${knowledgeBaseId}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -292,6 +306,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
// Silently fail
|
// Silently fail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.DOCUMENT_UPLOADED,
|
||||||
|
resourceType: AuditResourceType.DOCUMENT,
|
||||||
|
resourceId: knowledgeBaseId,
|
||||||
|
resourceName: validatedData.filename,
|
||||||
|
description: `Uploaded document "${validatedData.filename}" to knowledge base "${knowledgeBaseId}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: newDocument,
|
data: newDocument,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
auditMock,
|
||||||
createMockRequest,
|
createMockRequest,
|
||||||
mockAuth,
|
mockAuth,
|
||||||
mockConsoleLogger,
|
mockConsoleLogger,
|
||||||
@@ -16,6 +17,8 @@ mockKnowledgeSchemas()
|
|||||||
mockDrizzleOrm()
|
mockDrizzleOrm()
|
||||||
mockConsoleLogger()
|
mockConsoleLogger()
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
vi.mock('@/lib/knowledge/service', () => ({
|
vi.mock('@/lib/knowledge/service', () => ({
|
||||||
getKnowledgeBaseById: vi.fn(),
|
getKnowledgeBaseById: vi.fn(),
|
||||||
updateKnowledgeBase: vi.fn(),
|
updateKnowledgeBase: vi.fn(),
|
||||||
|
|||||||
@@ -1,6 +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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
@@ -135,6 +136,19 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Knowledge base updated: ${id} for user ${userId}`)
|
logger.info(`[${requestId}] Knowledge base updated: ${id} for user ${userId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: accessCheck.knowledgeBase.workspaceId ?? null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.KNOWLEDGE_BASE_UPDATED,
|
||||||
|
resourceType: AuditResourceType.KNOWLEDGE_BASE,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: validatedData.name ?? updatedKnowledgeBase.name,
|
||||||
|
description: `Updated knowledge base "${validatedData.name ?? updatedKnowledgeBase.name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: updatedKnowledgeBase,
|
data: updatedKnowledgeBase,
|
||||||
@@ -197,6 +211,19 @@ export async function DELETE(
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Knowledge base deleted: ${id} for user ${userId}`)
|
logger.info(`[${requestId}] Knowledge base deleted: ${id} for user ${userId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: accessCheck.knowledgeBase.workspaceId ?? null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.KNOWLEDGE_BASE_DELETED,
|
||||||
|
resourceType: AuditResourceType.KNOWLEDGE_BASE,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: accessCheck.knowledgeBase.name,
|
||||||
|
description: `Deleted knowledge base "${accessCheck.knowledgeBase.name || id}"`,
|
||||||
|
request: _request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: { message: 'Knowledge base deleted successfully' },
|
data: { message: 'Knowledge base deleted successfully' },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
auditMock,
|
||||||
createMockRequest,
|
createMockRequest,
|
||||||
mockAuth,
|
mockAuth,
|
||||||
mockConsoleLogger,
|
mockConsoleLogger,
|
||||||
@@ -16,6 +17,8 @@ mockKnowledgeSchemas()
|
|||||||
mockDrizzleOrm()
|
mockDrizzleOrm()
|
||||||
mockConsoleLogger()
|
mockConsoleLogger()
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
vi.mock('@/lib/workspaces/permissions/utils', () => ({
|
vi.mock('@/lib/workspaces/permissions/utils', () => ({
|
||||||
getUserEntityPermissions: vi.fn().mockResolvedValue('admin'),
|
getUserEntityPermissions: vi.fn().mockResolvedValue('admin'),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,6 +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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
@@ -109,6 +110,20 @@ export async function POST(req: NextRequest) {
|
|||||||
`[${requestId}] Knowledge base created: ${newKnowledgeBase.id} for user ${session.user.id}`
|
`[${requestId}] Knowledge base created: ${newKnowledgeBase.id} for user ${session.user.id}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: validatedData.workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.KNOWLEDGE_BASE_CREATED,
|
||||||
|
resourceType: AuditResourceType.KNOWLEDGE_BASE,
|
||||||
|
resourceId: newKnowledgeBase.id,
|
||||||
|
resourceName: validatedData.name,
|
||||||
|
description: `Created knowledge base "${validatedData.name}"`,
|
||||||
|
metadata: { name: validatedData.name },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: newKnowledgeBase,
|
data: newKnowledgeBase,
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export interface EmbeddingData {
|
|||||||
|
|
||||||
export interface KnowledgeBaseAccessResult {
|
export interface KnowledgeBaseAccessResult {
|
||||||
hasAccess: true
|
hasAccess: true
|
||||||
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId' | 'workspaceId'>
|
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId' | 'workspaceId' | 'name'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KnowledgeBaseAccessDenied {
|
export interface KnowledgeBaseAccessDenied {
|
||||||
@@ -113,7 +113,7 @@ export type KnowledgeBaseAccessCheck = KnowledgeBaseAccessResult | KnowledgeBase
|
|||||||
export interface DocumentAccessResult {
|
export interface DocumentAccessResult {
|
||||||
hasAccess: true
|
hasAccess: true
|
||||||
document: DocumentData
|
document: DocumentData
|
||||||
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId' | 'workspaceId'>
|
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId' | 'workspaceId' | 'name'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentAccessDenied {
|
export interface DocumentAccessDenied {
|
||||||
@@ -128,7 +128,7 @@ export interface ChunkAccessResult {
|
|||||||
hasAccess: true
|
hasAccess: true
|
||||||
chunk: EmbeddingData
|
chunk: EmbeddingData
|
||||||
document: DocumentData
|
document: DocumentData
|
||||||
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId' | 'workspaceId'>
|
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId' | 'workspaceId' | 'name'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChunkAccessDenied {
|
export interface ChunkAccessDenied {
|
||||||
@@ -151,6 +151,7 @@ export async function checkKnowledgeBaseAccess(
|
|||||||
id: knowledgeBase.id,
|
id: knowledgeBase.id,
|
||||||
userId: knowledgeBase.userId,
|
userId: knowledgeBase.userId,
|
||||||
workspaceId: knowledgeBase.workspaceId,
|
workspaceId: knowledgeBase.workspaceId,
|
||||||
|
name: knowledgeBase.name,
|
||||||
})
|
})
|
||||||
.from(knowledgeBase)
|
.from(knowledgeBase)
|
||||||
.where(and(eq(knowledgeBase.id, knowledgeBaseId), isNull(knowledgeBase.deletedAt)))
|
.where(and(eq(knowledgeBase.id, knowledgeBaseId), isNull(knowledgeBase.deletedAt)))
|
||||||
@@ -193,6 +194,7 @@ export async function checkKnowledgeBaseWriteAccess(
|
|||||||
id: knowledgeBase.id,
|
id: knowledgeBase.id,
|
||||||
userId: knowledgeBase.userId,
|
userId: knowledgeBase.userId,
|
||||||
workspaceId: knowledgeBase.workspaceId,
|
workspaceId: knowledgeBase.workspaceId,
|
||||||
|
name: knowledgeBase.name,
|
||||||
})
|
})
|
||||||
.from(knowledgeBase)
|
.from(knowledgeBase)
|
||||||
.where(and(eq(knowledgeBase.id, knowledgeBaseId), isNull(knowledgeBase.deletedAt)))
|
.where(and(eq(knowledgeBase.id, knowledgeBaseId), isNull(knowledgeBase.deletedAt)))
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { mcpServers } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, isNull } from 'drizzle-orm'
|
import { and, eq, isNull } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
|
import { McpDomainNotAllowedError, validateMcpDomain } from '@/lib/mcp/domain-check'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { mcpService } from '@/lib/mcp/service'
|
import { mcpService } from '@/lib/mcp/service'
|
||||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||||
@@ -15,7 +17,11 @@ export const dynamic = 'force-dynamic'
|
|||||||
* PATCH - Update an MCP server in the workspace (requires write or admin permission)
|
* PATCH - Update an MCP server in the workspace (requires write or admin permission)
|
||||||
*/
|
*/
|
||||||
export const PATCH = withMcpAuth<{ id: string }>('write')(
|
export const PATCH = withMcpAuth<{ id: string }>('write')(
|
||||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
async (
|
||||||
|
request: NextRequest,
|
||||||
|
{ userId, userName, userEmail, workspaceId, requestId },
|
||||||
|
{ params }
|
||||||
|
) => {
|
||||||
const { id: serverId } = await params
|
const { id: serverId } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -29,6 +35,17 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
|
|||||||
// Remove workspaceId from body to prevent it from being updated
|
// Remove workspaceId from body to prevent it from being updated
|
||||||
const { workspaceId: _, ...updateData } = body
|
const { workspaceId: _, ...updateData } = body
|
||||||
|
|
||||||
|
if (updateData.url) {
|
||||||
|
try {
|
||||||
|
validateMcpDomain(updateData.url)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof McpDomainNotAllowedError) {
|
||||||
|
return createMcpErrorResponse(e, e.message, 403)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the current server to check if URL is changing
|
// Get the current server to check if URL is changing
|
||||||
const [currentServer] = await db
|
const [currentServer] = await db
|
||||||
.select({ url: mcpServers.url })
|
.select({ url: mcpServers.url })
|
||||||
@@ -73,6 +90,20 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[${requestId}] Successfully updated MCP server: ${serverId}`)
|
logger.info(`[${requestId}] Successfully updated MCP server: ${serverId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: userName,
|
||||||
|
actorEmail: userEmail,
|
||||||
|
action: AuditAction.MCP_SERVER_UPDATED,
|
||||||
|
resourceType: AuditResourceType.MCP_SERVER,
|
||||||
|
resourceId: serverId,
|
||||||
|
resourceName: updatedServer.name || serverId,
|
||||||
|
description: `Updated MCP server "${updatedServer.name || serverId}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createMcpSuccessResponse({ server: updatedServer })
|
return createMcpSuccessResponse({ server: updatedServer })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error updating MCP server:`, error)
|
logger.error(`[${requestId}] Error updating MCP server:`, error)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { mcpServers } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, isNull } from 'drizzle-orm'
|
import { and, eq, isNull } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
|
import { McpDomainNotAllowedError, validateMcpDomain } from '@/lib/mcp/domain-check'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { mcpService } from '@/lib/mcp/service'
|
import { mcpService } from '@/lib/mcp/service'
|
||||||
import {
|
import {
|
||||||
@@ -54,7 +56,7 @@ export const GET = withMcpAuth('read')(
|
|||||||
* it will be updated instead of creating a duplicate.
|
* it will be updated instead of creating a duplicate.
|
||||||
*/
|
*/
|
||||||
export const POST = withMcpAuth('write')(
|
export const POST = withMcpAuth('write')(
|
||||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
|
||||||
try {
|
try {
|
||||||
const body = getParsedBody(request) || (await request.json())
|
const body = getParsedBody(request) || (await request.json())
|
||||||
|
|
||||||
@@ -72,6 +74,15 @@ export const POST = withMcpAuth('write')(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateMcpDomain(body.url)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof McpDomainNotAllowedError) {
|
||||||
|
return createMcpErrorResponse(e, e.message, 403)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
const serverId = body.url ? generateMcpServerId(workspaceId, body.url) : crypto.randomUUID()
|
const serverId = body.url ? generateMcpServerId(workspaceId, body.url) : crypto.randomUUID()
|
||||||
|
|
||||||
const [existingServer] = await db
|
const [existingServer] = await db
|
||||||
@@ -151,6 +162,20 @@ export const POST = withMcpAuth('write')(
|
|||||||
// Silently fail
|
// Silently fail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: userName,
|
||||||
|
actorEmail: userEmail,
|
||||||
|
action: AuditAction.MCP_SERVER_ADDED,
|
||||||
|
resourceType: AuditResourceType.MCP_SERVER,
|
||||||
|
resourceId: serverId,
|
||||||
|
resourceName: body.name,
|
||||||
|
description: `Added MCP server "${body.name}"`,
|
||||||
|
metadata: { serverName: body.name, transport: body.transport },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createMcpSuccessResponse({ serverId }, 201)
|
return createMcpSuccessResponse({ serverId }, 201)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error registering MCP server:`, error)
|
logger.error(`[${requestId}] Error registering MCP server:`, error)
|
||||||
@@ -167,7 +192,7 @@ export const POST = withMcpAuth('write')(
|
|||||||
* DELETE - Delete an MCP server from the workspace (requires admin permission)
|
* DELETE - Delete an MCP server from the workspace (requires admin permission)
|
||||||
*/
|
*/
|
||||||
export const DELETE = withMcpAuth('admin')(
|
export const DELETE = withMcpAuth('admin')(
|
||||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url)
|
||||||
const serverId = searchParams.get('serverId')
|
const serverId = searchParams.get('serverId')
|
||||||
@@ -198,6 +223,20 @@ export const DELETE = withMcpAuth('admin')(
|
|||||||
await mcpService.clearCache(workspaceId)
|
await mcpService.clearCache(workspaceId)
|
||||||
|
|
||||||
logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
|
logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: userName,
|
||||||
|
actorEmail: userEmail,
|
||||||
|
action: AuditAction.MCP_SERVER_REMOVED,
|
||||||
|
resourceType: AuditResourceType.MCP_SERVER,
|
||||||
|
resourceId: serverId!,
|
||||||
|
resourceName: deletedServer.name,
|
||||||
|
description: `Removed MCP server "${deletedServer.name}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
|
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error deleting MCP server:`, error)
|
logger.error(`[${requestId}] Error deleting MCP server:`, error)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { McpClient } from '@/lib/mcp/client'
|
import { McpClient } from '@/lib/mcp/client'
|
||||||
|
import { McpDomainNotAllowedError, validateMcpDomain } from '@/lib/mcp/domain-check'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config'
|
import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config'
|
||||||
import type { McpTransport } from '@/lib/mcp/types'
|
import type { McpTransport } from '@/lib/mcp/types'
|
||||||
@@ -71,6 +72,15 @@ export const POST = withMcpAuth('write')(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateMcpDomain(body.url)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof McpDomainNotAllowedError) {
|
||||||
|
return createMcpErrorResponse(e, e.message, 403)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
// Build initial config for resolution
|
// Build initial config for resolution
|
||||||
const initialConfig = {
|
const initialConfig = {
|
||||||
id: `test-${requestId}`,
|
id: `test-${requestId}`,
|
||||||
@@ -95,6 +105,16 @@ export const POST = withMcpAuth('write')(
|
|||||||
logger.warn(`[${requestId}] Some environment variables not found:`, { missingVars })
|
logger.warn(`[${requestId}] Some environment variables not found:`, { missingVars })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-validate domain after env var resolution
|
||||||
|
try {
|
||||||
|
validateMcpDomain(testConfig.url)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof McpDomainNotAllowedError) {
|
||||||
|
return createMcpErrorResponse(e, e.message, 403)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
const testSecurityPolicy = {
|
const testSecurityPolicy = {
|
||||||
requireConsent: false,
|
requireConsent: false,
|
||||||
auditLevel: 'none' as const,
|
auditLevel: 'none' as const,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { mcpPubSub } from '@/lib/mcp/pubsub'
|
import { mcpPubSub } from '@/lib/mcp/pubsub'
|
||||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||||
@@ -71,7 +72,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
|
|||||||
* PATCH - Update a workflow MCP server
|
* PATCH - Update a workflow MCP server
|
||||||
*/
|
*/
|
||||||
export const PATCH = withMcpAuth<RouteParams>('write')(
|
export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
async (
|
||||||
|
request: NextRequest,
|
||||||
|
{ userId, userName, userEmail, workspaceId, requestId },
|
||||||
|
{ params }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const { id: serverId } = await params
|
const { id: serverId } = await params
|
||||||
const body = getParsedBody(request) || (await request.json())
|
const body = getParsedBody(request) || (await request.json())
|
||||||
@@ -112,6 +117,19 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Successfully updated workflow MCP server: ${serverId}`)
|
logger.info(`[${requestId}] Successfully updated workflow MCP server: ${serverId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: userName,
|
||||||
|
actorEmail: userEmail,
|
||||||
|
action: AuditAction.MCP_SERVER_UPDATED,
|
||||||
|
resourceType: AuditResourceType.MCP_SERVER,
|
||||||
|
resourceId: serverId,
|
||||||
|
resourceName: updatedServer.name,
|
||||||
|
description: `Updated workflow MCP server "${updatedServer.name}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createMcpSuccessResponse({ server: updatedServer })
|
return createMcpSuccessResponse({ server: updatedServer })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error updating workflow MCP server:`, error)
|
logger.error(`[${requestId}] Error updating workflow MCP server:`, error)
|
||||||
@@ -128,7 +146,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
|||||||
* DELETE - Delete a workflow MCP server and all its tools
|
* DELETE - Delete a workflow MCP server and all its tools
|
||||||
*/
|
*/
|
||||||
export const DELETE = withMcpAuth<RouteParams>('admin')(
|
export const DELETE = withMcpAuth<RouteParams>('admin')(
|
||||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
async (
|
||||||
|
request: NextRequest,
|
||||||
|
{ userId, userName, userEmail, workspaceId, requestId },
|
||||||
|
{ params }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const { id: serverId } = await params
|
const { id: serverId } = await params
|
||||||
|
|
||||||
@@ -149,6 +171,19 @@ export const DELETE = withMcpAuth<RouteParams>('admin')(
|
|||||||
|
|
||||||
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId })
|
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId })
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: userName,
|
||||||
|
actorEmail: userEmail,
|
||||||
|
action: AuditAction.MCP_SERVER_REMOVED,
|
||||||
|
resourceType: AuditResourceType.MCP_SERVER,
|
||||||
|
resourceId: serverId,
|
||||||
|
resourceName: deletedServer.name,
|
||||||
|
description: `Unpublished workflow MCP server "${deletedServer.name}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
|
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error deleting workflow MCP server:`, error)
|
logger.error(`[${requestId}] Error deleting workflow MCP server:`, error)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { mcpPubSub } from '@/lib/mcp/pubsub'
|
import { mcpPubSub } from '@/lib/mcp/pubsub'
|
||||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||||
@@ -65,7 +66,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
|
|||||||
* PATCH - Update a tool's configuration
|
* PATCH - Update a tool's configuration
|
||||||
*/
|
*/
|
||||||
export const PATCH = withMcpAuth<RouteParams>('write')(
|
export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
async (
|
||||||
|
request: NextRequest,
|
||||||
|
{ userId, userName, userEmail, workspaceId, requestId },
|
||||||
|
{ params }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const { id: serverId, toolId } = await params
|
const { id: serverId, toolId } = await params
|
||||||
const body = getParsedBody(request) || (await request.json())
|
const body = getParsedBody(request) || (await request.json())
|
||||||
@@ -118,6 +123,19 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
|||||||
|
|
||||||
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId })
|
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId })
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: userName,
|
||||||
|
actorEmail: userEmail,
|
||||||
|
action: AuditAction.MCP_SERVER_UPDATED,
|
||||||
|
resourceType: AuditResourceType.MCP_SERVER,
|
||||||
|
resourceId: serverId,
|
||||||
|
description: `Updated tool "${updatedTool.toolName}" in MCP server`,
|
||||||
|
metadata: { toolId, toolName: updatedTool.toolName },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createMcpSuccessResponse({ tool: updatedTool })
|
return createMcpSuccessResponse({ tool: updatedTool })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error updating tool:`, error)
|
logger.error(`[${requestId}] Error updating tool:`, error)
|
||||||
@@ -134,7 +152,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
|||||||
* DELETE - Remove a tool from an MCP server
|
* DELETE - Remove a tool from an MCP server
|
||||||
*/
|
*/
|
||||||
export const DELETE = withMcpAuth<RouteParams>('write')(
|
export const DELETE = withMcpAuth<RouteParams>('write')(
|
||||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
async (
|
||||||
|
request: NextRequest,
|
||||||
|
{ userId, userName, userEmail, workspaceId, requestId },
|
||||||
|
{ params }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const { id: serverId, toolId } = await params
|
const { id: serverId, toolId } = await params
|
||||||
|
|
||||||
@@ -165,6 +187,19 @@ export const DELETE = withMcpAuth<RouteParams>('write')(
|
|||||||
|
|
||||||
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId })
|
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId })
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: userName,
|
||||||
|
actorEmail: userEmail,
|
||||||
|
action: AuditAction.MCP_SERVER_UPDATED,
|
||||||
|
resourceType: AuditResourceType.MCP_SERVER,
|
||||||
|
resourceId: serverId,
|
||||||
|
description: `Removed tool "${deletedTool.toolName}" from MCP server`,
|
||||||
|
metadata: { toolId, toolName: deletedTool.toolName },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createMcpSuccessResponse({ message: `Tool ${toolId} deleted successfully` })
|
return createMcpSuccessResponse({ message: `Tool ${toolId} deleted successfully` })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error deleting tool:`, error)
|
logger.error(`[${requestId}] Error deleting tool:`, error)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { mcpPubSub } from '@/lib/mcp/pubsub'
|
import { mcpPubSub } from '@/lib/mcp/pubsub'
|
||||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||||
@@ -76,7 +77,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
|
|||||||
* POST - Add a workflow as a tool to an MCP server
|
* POST - Add a workflow as a tool to an MCP server
|
||||||
*/
|
*/
|
||||||
export const POST = withMcpAuth<RouteParams>('write')(
|
export const POST = withMcpAuth<RouteParams>('write')(
|
||||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
async (
|
||||||
|
request: NextRequest,
|
||||||
|
{ userId, userName, userEmail, workspaceId, requestId },
|
||||||
|
{ params }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const { id: serverId } = await params
|
const { id: serverId } = await params
|
||||||
const body = getParsedBody(request) || (await request.json())
|
const body = getParsedBody(request) || (await request.json())
|
||||||
@@ -197,6 +202,19 @@ export const POST = withMcpAuth<RouteParams>('write')(
|
|||||||
|
|
||||||
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId })
|
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId })
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: userName,
|
||||||
|
actorEmail: userEmail,
|
||||||
|
action: AuditAction.MCP_SERVER_UPDATED,
|
||||||
|
resourceType: AuditResourceType.MCP_SERVER,
|
||||||
|
resourceId: serverId,
|
||||||
|
description: `Added tool "${toolName}" to MCP server`,
|
||||||
|
metadata: { toolId, toolName, workflowId: body.workflowId },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createMcpSuccessResponse({ tool }, 201)
|
return createMcpSuccessResponse({ tool }, 201)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error adding tool:`, error)
|
logger.error(`[${requestId}] Error adding tool:`, error)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { eq, inArray, sql } from 'drizzle-orm'
|
import { eq, inArray, sql } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { mcpPubSub } from '@/lib/mcp/pubsub'
|
import { mcpPubSub } from '@/lib/mcp/pubsub'
|
||||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||||
@@ -85,7 +86,7 @@ export const GET = withMcpAuth('read')(
|
|||||||
* POST - Create a new workflow MCP server
|
* POST - Create a new workflow MCP server
|
||||||
*/
|
*/
|
||||||
export const POST = withMcpAuth('write')(
|
export const POST = withMcpAuth('write')(
|
||||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
|
||||||
try {
|
try {
|
||||||
const body = getParsedBody(request) || (await request.json())
|
const body = getParsedBody(request) || (await request.json())
|
||||||
|
|
||||||
@@ -188,6 +189,19 @@ export const POST = withMcpAuth('write')(
|
|||||||
`[${requestId}] Successfully created workflow MCP server: ${body.name} (ID: ${serverId})`
|
`[${requestId}] Successfully created workflow MCP server: ${body.name} (ID: ${serverId})`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: userName,
|
||||||
|
actorEmail: userEmail,
|
||||||
|
action: AuditAction.MCP_SERVER_ADDED,
|
||||||
|
resourceType: AuditResourceType.MCP_SERVER,
|
||||||
|
resourceId: serverId,
|
||||||
|
resourceName: body.name.trim(),
|
||||||
|
description: `Published workflow MCP server "${body.name.trim()}" with ${addedTools.length} tool(s)`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createMcpSuccessResponse({ server, addedTools }, 201)
|
return createMcpSuccessResponse({ server, addedTools }, 201)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error creating workflow MCP server:`, error)
|
logger.error(`[${requestId}] Error creating workflow MCP server:`, error)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { and, eq } 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 { getEmailSubject, renderInvitationEmail } from '@/components/emails'
|
import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasAccessControlAccess } from '@/lib/billing'
|
import { hasAccessControlAccess } from '@/lib/billing'
|
||||||
import { syncUsageLimitsFromSubscription } from '@/lib/billing/core/usage'
|
import { syncUsageLimitsFromSubscription } from '@/lib/billing/core/usage'
|
||||||
@@ -552,6 +553,25 @@ export async function PUT(
|
|||||||
email: orgInvitation.email,
|
email: orgInvitation.email,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const auditActionMap = {
|
||||||
|
accepted: AuditAction.ORG_INVITATION_ACCEPTED,
|
||||||
|
rejected: AuditAction.ORG_INVITATION_REJECTED,
|
||||||
|
cancelled: AuditAction.ORG_INVITATION_CANCELLED,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: auditActionMap[status],
|
||||||
|
resourceType: AuditResourceType.ORGANIZATION,
|
||||||
|
resourceId: organizationId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Organization invitation ${status} for ${orgInvitation.email}`,
|
||||||
|
metadata: { invitationId, email: orgInvitation.email, status },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Invitation ${status} successfully`,
|
message: `Invitation ${status} successfully`,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
renderBatchInvitationEmail,
|
renderBatchInvitationEmail,
|
||||||
renderInvitationEmail,
|
renderInvitationEmail,
|
||||||
} from '@/components/emails'
|
} from '@/components/emails'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import {
|
import {
|
||||||
validateBulkInvitations,
|
validateBulkInvitations,
|
||||||
@@ -411,6 +412,22 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
workspaceInvitationCount: workspaceInvitationIds.length,
|
workspaceInvitationCount: workspaceInvitationIds.length,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const inv of invitationsToCreate) {
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.ORG_INVITATION_CREATED,
|
||||||
|
resourceType: AuditResourceType.ORGANIZATION,
|
||||||
|
resourceId: organizationId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: organizationEntry[0]?.name,
|
||||||
|
description: `Invited ${inv.email} to organization as ${role}`,
|
||||||
|
metadata: { invitationId: inv.id, email: inv.email, role },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `${invitationsToCreate.length} invitation(s) sent successfully`,
|
message: `${invitationsToCreate.length} invitation(s) sent successfully`,
|
||||||
@@ -532,6 +549,19 @@ export async function DELETE(
|
|||||||
email: result[0].email,
|
email: result[0].email,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.ORG_INVITATION_REVOKED,
|
||||||
|
resourceType: AuditResourceType.ORGANIZATION,
|
||||||
|
resourceId: organizationId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Revoked organization invitation for ${result[0].email}`,
|
||||||
|
metadata: { invitationId, email: result[0].email },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Invitation cancelled successfully',
|
message: 'Invitation cancelled successfully',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { getUserUsageData } from '@/lib/billing/core/usage'
|
import { getUserUsageData } from '@/lib/billing/core/usage'
|
||||||
import { removeUserFromOrganization } from '@/lib/billing/organizations/membership'
|
import { removeUserFromOrganization } from '@/lib/billing/organizations/membership'
|
||||||
@@ -213,6 +214,19 @@ export async function PUT(
|
|||||||
updatedBy: session.user.id,
|
updatedBy: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.ORG_MEMBER_ROLE_CHANGED,
|
||||||
|
resourceType: AuditResourceType.ORGANIZATION,
|
||||||
|
resourceId: organizationId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Changed role for member ${memberId} to ${role}`,
|
||||||
|
metadata: { targetUserId: memberId, newRole: role },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Member role updated successfully',
|
message: 'Member role updated successfully',
|
||||||
@@ -305,6 +319,22 @@ export async function DELETE(
|
|||||||
billingActions: result.billingActions,
|
billingActions: result.billingActions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.ORG_MEMBER_REMOVED,
|
||||||
|
resourceType: AuditResourceType.ORGANIZATION,
|
||||||
|
resourceId: organizationId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description:
|
||||||
|
session.user.id === targetUserId
|
||||||
|
? 'Left the organization'
|
||||||
|
: `Removed member ${targetUserId} from organization`,
|
||||||
|
metadata: { targetUserId, wasSelfRemoval: session.user.id === targetUserId },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message:
|
message:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
|
import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { getUserUsageData } from '@/lib/billing/core/usage'
|
import { getUserUsageData } from '@/lib/billing/core/usage'
|
||||||
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
|
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
|
||||||
@@ -285,6 +286,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
// Don't fail the request if email fails
|
// Don't fail the request if email fails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.ORG_INVITATION_CREATED,
|
||||||
|
resourceType: AuditResourceType.ORGANIZATION,
|
||||||
|
resourceId: organizationId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Invited ${normalizedEmail} to organization as ${role}`,
|
||||||
|
metadata: { invitationId, email: normalizedEmail, role },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Invitation sent to ${normalizedEmail}`,
|
message: `Invitation sent to ${normalizedEmail}`,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq, ne } from 'drizzle-orm'
|
import { and, eq, ne } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import {
|
import {
|
||||||
getOrganizationSeatAnalytics,
|
getOrganizationSeatAnalytics,
|
||||||
@@ -192,6 +193,20 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
changes: { name, slug, logo },
|
changes: { name, slug, logo },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.ORGANIZATION_UPDATED,
|
||||||
|
resourceType: AuditResourceType.ORGANIZATION,
|
||||||
|
resourceId: organizationId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: updatedOrg[0].name,
|
||||||
|
description: `Updated organization settings`,
|
||||||
|
metadata: { changes: { name, slug, logo } },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Organization updated successfully',
|
message: 'Organization updated successfully',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { member, organization } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, or } from 'drizzle-orm'
|
import { and, eq, or } from 'drizzle-orm'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { createOrganizationForTeamPlan } from '@/lib/billing/organization'
|
import { createOrganizationForTeamPlan } from '@/lib/billing/organization'
|
||||||
|
|
||||||
@@ -115,6 +116,19 @@ export async function POST(request: Request) {
|
|||||||
organizationId,
|
organizationId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: user.id,
|
||||||
|
action: AuditAction.ORGANIZATION_CREATED,
|
||||||
|
resourceType: AuditResourceType.ORGANIZATION,
|
||||||
|
resourceId: organizationId,
|
||||||
|
actorName: user.name ?? undefined,
|
||||||
|
actorEmail: user.email ?? undefined,
|
||||||
|
resourceName: organizationName ?? undefined,
|
||||||
|
description: `Created organization "${organizationName}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
organizationId,
|
organizationId,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasAccessControlAccess } from '@/lib/billing'
|
import { hasAccessControlAccess } from '@/lib/billing'
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ async function getPermissionGroupWithAccess(groupId: string, userId: string) {
|
|||||||
const [group] = await db
|
const [group] = await db
|
||||||
.select({
|
.select({
|
||||||
id: permissionGroup.id,
|
id: permissionGroup.id,
|
||||||
|
name: permissionGroup.name,
|
||||||
organizationId: permissionGroup.organizationId,
|
organizationId: permissionGroup.organizationId,
|
||||||
})
|
})
|
||||||
.from(permissionGroup)
|
.from(permissionGroup)
|
||||||
@@ -151,6 +153,20 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
assignedBy: session.user.id,
|
assignedBy: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.PERMISSION_GROUP_MEMBER_ADDED,
|
||||||
|
resourceType: AuditResourceType.PERMISSION_GROUP,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: result.group.name,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Added member ${userId} to permission group "${result.group.name}"`,
|
||||||
|
metadata: { targetUserId: userId, permissionGroupId: id },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ member: newMember }, { status: 201 })
|
return NextResponse.json({ member: newMember }, { status: 201 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
@@ -221,6 +237,20 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.PERMISSION_GROUP_MEMBER_REMOVED,
|
||||||
|
resourceType: AuditResourceType.PERMISSION_GROUP,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: result.group.name,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Removed member ${memberToRemove.userId} from permission group "${result.group.name}"`,
|
||||||
|
metadata: { targetUserId: memberToRemove.userId, memberId, permissionGroupId: id },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error removing member from permission group', error)
|
logger.error('Error removing member from permission group', error)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasAccessControlAccess } from '@/lib/billing'
|
import { hasAccessControlAccess } from '@/lib/billing'
|
||||||
import {
|
import {
|
||||||
@@ -181,6 +182,19 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
.where(eq(permissionGroup.id, id))
|
.where(eq(permissionGroup.id, id))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.PERMISSION_GROUP_UPDATED,
|
||||||
|
resourceType: AuditResourceType.PERMISSION_GROUP,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: updated.name,
|
||||||
|
description: `Updated permission group "${updated.name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
permissionGroup: {
|
permissionGroup: {
|
||||||
...updated,
|
...updated,
|
||||||
@@ -229,6 +243,19 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
|||||||
|
|
||||||
logger.info('Deleted permission group', { permissionGroupId: id, userId: session.user.id })
|
logger.info('Deleted permission group', { permissionGroupId: id, userId: session.user.id })
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.PERMISSION_GROUP_DELETED,
|
||||||
|
resourceType: AuditResourceType.PERMISSION_GROUP,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: result.group.name,
|
||||||
|
description: `Deleted permission group "${result.group.name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error deleting permission group', error)
|
logger.error('Error deleting permission group', error)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, count, desc, eq } from 'drizzle-orm'
|
import { and, count, desc, eq } from 'drizzle-orm'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasAccessControlAccess } from '@/lib/billing'
|
import { hasAccessControlAccess } from '@/lib/billing'
|
||||||
import {
|
import {
|
||||||
@@ -198,6 +199,19 @@ export async function POST(req: Request) {
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.PERMISSION_GROUP_CREATED,
|
||||||
|
resourceType: AuditResourceType.PERMISSION_GROUP,
|
||||||
|
resourceId: newGroup.id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: name,
|
||||||
|
description: `Created permission group "${name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ permissionGroup: newGroup }, { status: 201 })
|
return NextResponse.json({ permissionGroup: newGroup }, { status: 201 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import { databaseMock, loggerMock } from '@sim/testing'
|
import { auditMock, databaseMock, loggerMock } from '@sim/testing'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
@@ -37,6 +37,8 @@ vi.mock('@/lib/core/utils/request', () => ({
|
|||||||
|
|
||||||
vi.mock('@sim/logger', () => loggerMock)
|
vi.mock('@sim/logger', () => loggerMock)
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
import { PUT } from './route'
|
import { PUT } from './route'
|
||||||
|
|
||||||
function createRequest(body: Record<string, unknown>): NextRequest {
|
function createRequest(body: Record<string, unknown>): NextRequest {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { validateCronExpression } from '@/lib/workflows/schedules/utils'
|
import { validateCronExpression } from '@/lib/workflows/schedules/utils'
|
||||||
@@ -106,6 +107,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Reactivated schedule: ${scheduleId}`)
|
logger.info(`[${requestId}] Reactivated schedule: ${scheduleId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: authorization.workflow.workspaceId ?? null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.SCHEDULE_UPDATED,
|
||||||
|
resourceType: AuditResourceType.SCHEDULE,
|
||||||
|
resourceId: scheduleId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Reactivated schedule for workflow ${schedule.workflowId}`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: 'Schedule activated successfully',
|
message: 'Schedule activated successfully',
|
||||||
nextRunAt,
|
nextRunAt,
|
||||||
|
|||||||
14
apps/sim/app/api/settings/allowed-integrations/route.ts
Normal file
14
apps/sim/app/api/settings/allowed-integrations/route.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
|
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await getSession()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
allowedIntegrations: getAllowedIntegrationsFromEnv(),
|
||||||
|
})
|
||||||
|
}
|
||||||
27
apps/sim/app/api/settings/allowed-mcp-domains/route.ts
Normal file
27
apps/sim/app/api/settings/allowed-mcp-domains/route.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
|
import { getAllowedMcpDomainsFromEnv } from '@/lib/core/config/feature-flags'
|
||||||
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await getSession()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredDomains = getAllowedMcpDomainsFromEnv()
|
||||||
|
if (configuredDomains === null) {
|
||||||
|
return NextResponse.json({ allowedMcpDomains: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const platformHostname = new URL(getBaseUrl()).hostname.toLowerCase()
|
||||||
|
if (!configuredDomains.includes(platformHostname)) {
|
||||||
|
return NextResponse.json({
|
||||||
|
allowedMcpDomains: [...configuredDomains, platformHostname],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return NextResponse.json({ allowedMcpDomains: configuredDomains })
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { eq, sql } from 'drizzle-orm'
|
import { eq, sql } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import {
|
import {
|
||||||
@@ -247,6 +248,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Successfully updated template: ${id}`)
|
logger.info(`[${requestId}] Successfully updated template: ${id}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.TEMPLATE_UPDATED,
|
||||||
|
resourceType: AuditResourceType.TEMPLATE,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: name ?? template.name,
|
||||||
|
description: `Updated template "${name ?? template.name}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
data: updatedTemplate[0],
|
data: updatedTemplate[0],
|
||||||
message: 'Template updated successfully',
|
message: 'Template updated successfully',
|
||||||
@@ -300,6 +313,19 @@ export async function DELETE(
|
|||||||
await db.delete(templates).where(eq(templates.id, id))
|
await db.delete(templates).where(eq(templates.id, id))
|
||||||
|
|
||||||
logger.info(`[${requestId}] Deleted template: ${id}`)
|
logger.info(`[${requestId}] Deleted template: ${id}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.TEMPLATE_DELETED,
|
||||||
|
resourceType: AuditResourceType.TEMPLATE,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: template.name,
|
||||||
|
description: `Deleted template "${template.name}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`[${requestId}] Error deleting template: ${id}`, error)
|
logger.error(`[${requestId}] Error deleting template: ${id}`, error)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { and, desc, eq, ilike, or, sql } from 'drizzle-orm'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
|
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
|
||||||
@@ -285,6 +286,18 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Successfully created template: ${templateId}`)
|
logger.info(`[${requestId}] Successfully created template: ${templateId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.TEMPLATE_CREATED,
|
||||||
|
resourceType: AuditResourceType.TEMPLATE,
|
||||||
|
resourceId: templateId,
|
||||||
|
resourceName: data.name,
|
||||||
|
description: `Created template "${data.name}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
id: templateId,
|
id: templateId,
|
||||||
|
|||||||
@@ -22,15 +22,20 @@ interface PipedriveFile {
|
|||||||
interface PipedriveApiResponse {
|
interface PipedriveApiResponse {
|
||||||
success: boolean
|
success: boolean
|
||||||
data?: PipedriveFile[]
|
data?: PipedriveFile[]
|
||||||
|
additional_data?: {
|
||||||
|
pagination?: {
|
||||||
|
more_items_in_collection: boolean
|
||||||
|
next_start: number
|
||||||
|
}
|
||||||
|
}
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const PipedriveGetFilesSchema = z.object({
|
const PipedriveGetFilesSchema = z.object({
|
||||||
accessToken: z.string().min(1, 'Access token is required'),
|
accessToken: z.string().min(1, 'Access token is required'),
|
||||||
deal_id: z.string().optional().nullable(),
|
sort: z.enum(['id', 'update_time']).optional().nullable(),
|
||||||
person_id: z.string().optional().nullable(),
|
|
||||||
org_id: z.string().optional().nullable(),
|
|
||||||
limit: z.string().optional().nullable(),
|
limit: z.string().optional().nullable(),
|
||||||
|
start: z.string().optional().nullable(),
|
||||||
downloadFiles: z.boolean().optional().default(false),
|
downloadFiles: z.boolean().optional().default(false),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -54,20 +59,19 @@ export async function POST(request: NextRequest) {
|
|||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const validatedData = PipedriveGetFilesSchema.parse(body)
|
const validatedData = PipedriveGetFilesSchema.parse(body)
|
||||||
|
|
||||||
const { accessToken, deal_id, person_id, org_id, limit, downloadFiles } = validatedData
|
const { accessToken, sort, limit, start, downloadFiles } = validatedData
|
||||||
|
|
||||||
const baseUrl = 'https://api.pipedrive.com/v1/files'
|
const baseUrl = 'https://api.pipedrive.com/v1/files'
|
||||||
const queryParams = new URLSearchParams()
|
const queryParams = new URLSearchParams()
|
||||||
|
|
||||||
if (deal_id) queryParams.append('deal_id', deal_id)
|
if (sort) queryParams.append('sort', sort)
|
||||||
if (person_id) queryParams.append('person_id', person_id)
|
|
||||||
if (org_id) queryParams.append('org_id', org_id)
|
|
||||||
if (limit) queryParams.append('limit', limit)
|
if (limit) queryParams.append('limit', limit)
|
||||||
|
if (start) queryParams.append('start', start)
|
||||||
|
|
||||||
const queryString = queryParams.toString()
|
const queryString = queryParams.toString()
|
||||||
const apiUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl
|
const apiUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl
|
||||||
|
|
||||||
logger.info(`[${requestId}] Fetching files from Pipedrive`, { deal_id, person_id, org_id })
|
logger.info(`[${requestId}] Fetching files from Pipedrive`)
|
||||||
|
|
||||||
const urlValidation = await validateUrlWithDNS(apiUrl, 'apiUrl')
|
const urlValidation = await validateUrlWithDNS(apiUrl, 'apiUrl')
|
||||||
if (!urlValidation.isValid) {
|
if (!urlValidation.isValid) {
|
||||||
@@ -93,6 +97,8 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const files = data.data || []
|
const files = data.data || []
|
||||||
|
const hasMore = data.additional_data?.pagination?.more_items_in_collection || false
|
||||||
|
const nextStart = data.additional_data?.pagination?.next_start ?? null
|
||||||
const downloadedFiles: Array<{
|
const downloadedFiles: Array<{
|
||||||
name: string
|
name: string
|
||||||
mimeType: string
|
mimeType: string
|
||||||
@@ -149,6 +155,8 @@ export async function POST(request: NextRequest) {
|
|||||||
files,
|
files,
|
||||||
downloadedFiles: downloadedFiles.length > 0 ? downloadedFiles : undefined,
|
downloadedFiles: downloadedFiles.length > 0 ? downloadedFiles : undefined,
|
||||||
total_items: files.length,
|
total_items: files.length,
|
||||||
|
has_more: hasMore,
|
||||||
|
next_start: nextStart,
|
||||||
success: true,
|
success: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { apiKey } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -34,12 +35,27 @@ export async function DELETE(
|
|||||||
const result = await db
|
const result = await db
|
||||||
.delete(apiKey)
|
.delete(apiKey)
|
||||||
.where(and(eq(apiKey.id, keyId), eq(apiKey.userId, userId)))
|
.where(and(eq(apiKey.id, keyId), eq(apiKey.userId, userId)))
|
||||||
.returning({ id: apiKey.id })
|
.returning({ id: apiKey.id, name: apiKey.name })
|
||||||
|
|
||||||
if (!result.length) {
|
if (!result.length) {
|
||||||
return NextResponse.json({ error: 'API key not found' }, { status: 404 })
|
return NextResponse.json({ error: 'API key not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deletedKey = result[0]
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: userId,
|
||||||
|
action: AuditAction.PERSONAL_API_KEY_REVOKED,
|
||||||
|
resourceType: AuditResourceType.API_KEY,
|
||||||
|
resourceId: keyId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: deletedKey.name,
|
||||||
|
description: `Revoked personal API key: ${deletedKey.name}`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to delete API key', { error })
|
logger.error('Failed to delete API key', { error })
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
|
|||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth'
|
import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
|
|
||||||
const logger = createLogger('ApiKeysAPI')
|
const logger = createLogger('ApiKeysAPI')
|
||||||
@@ -110,6 +111,19 @@ export async function POST(request: NextRequest) {
|
|||||||
createdAt: apiKey.createdAt,
|
createdAt: apiKey.createdAt,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: userId,
|
||||||
|
action: AuditAction.PERSONAL_API_KEY_CREATED,
|
||||||
|
resourceType: AuditResourceType.API_KEY,
|
||||||
|
resourceId: newKey.id,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: name,
|
||||||
|
description: `Created personal API key: ${name}`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
key: {
|
key: {
|
||||||
...newKey,
|
...newKey,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { webhook, workflow } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateInteger } from '@/lib/core/security/input-validation'
|
import { validateInteger } from '@/lib/core/security/input-validation'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
@@ -261,6 +262,20 @@ export async function DELETE(
|
|||||||
logger.info(`[${requestId}] Successfully deleted webhook: ${id}`)
|
logger.info(`[${requestId}] Successfully deleted webhook: ${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: webhookData.workflow.workspaceId || null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.WEBHOOK_DELETED,
|
||||||
|
resourceType: AuditResourceType.WEBHOOK,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: foundWebhook.provider || 'generic',
|
||||||
|
description: 'Deleted webhook',
|
||||||
|
metadata: { workflowId: webhookData.workflow.id },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 })
|
return NextResponse.json({ success: true }, { status: 200 })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`[${requestId}] Error deleting webhook`, {
|
logger.error(`[${requestId}] Error deleting webhook`, {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, desc, eq, inArray, isNull, or } from 'drizzle-orm'
|
import { and, desc, eq, inArray, isNull, or } from 'drizzle-orm'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
@@ -145,7 +146,8 @@ export async function GET(request: NextRequest) {
|
|||||||
// Create or Update a webhook
|
// Create or Update a webhook
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
const userId = (await getSession())?.user?.id
|
const session = await getSession()
|
||||||
|
const userId = session?.user?.id
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized webhook creation attempt`)
|
logger.warn(`[${requestId}] Unauthorized webhook creation attempt`)
|
||||||
@@ -678,6 +680,20 @@ export async function POST(request: NextRequest) {
|
|||||||
} catch {
|
} catch {
|
||||||
// Telemetry should not fail the operation
|
// Telemetry should not fail the operation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workflowRecord.workspaceId || null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: session?.user?.name ?? undefined,
|
||||||
|
actorEmail: session?.user?.email ?? undefined,
|
||||||
|
action: AuditAction.WEBHOOK_CREATED,
|
||||||
|
resourceType: AuditResourceType.WEBHOOK,
|
||||||
|
resourceId: savedWebhook.id,
|
||||||
|
resourceName: provider || 'generic',
|
||||||
|
description: `Created ${provider || 'generic'} webhook`,
|
||||||
|
metadata: { provider, workflowId },
|
||||||
|
request,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = targetWebhookId ? 200 : 201
|
const status = targetWebhookId ? 200 : 201
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, desc, eq } from 'drizzle-orm'
|
import { and, desc, eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { removeMcpToolsForWorkflow, syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
import { removeMcpToolsForWorkflow, syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
||||||
import {
|
import {
|
||||||
@@ -258,6 +259,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
// Sync MCP tools with the latest parameter schema
|
// Sync MCP tools with the latest parameter schema
|
||||||
await syncMcpToolsForWorkflow({ workflowId: id, requestId, context: 'deploy' })
|
await syncMcpToolsForWorkflow({ workflowId: id, requestId, context: 'deploy' })
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workflowData?.workspaceId || null,
|
||||||
|
actorId: actorUserId,
|
||||||
|
actorName: session?.user?.name,
|
||||||
|
actorEmail: session?.user?.email,
|
||||||
|
action: AuditAction.WORKFLOW_DEPLOYED,
|
||||||
|
resourceType: AuditResourceType.WORKFLOW,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: workflowData?.name,
|
||||||
|
description: `Deployed workflow "${workflowData?.name || id}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
const responseApiKeyInfo = workflowData!.workspaceId
|
const responseApiKeyInfo = workflowData!.workspaceId
|
||||||
? 'Workspace API keys'
|
? 'Workspace API keys'
|
||||||
: 'Personal API keys'
|
: 'Personal API keys'
|
||||||
@@ -297,11 +311,11 @@ export async function DELETE(
|
|||||||
try {
|
try {
|
||||||
logger.debug(`[${requestId}] Undeploying workflow: ${id}`)
|
logger.debug(`[${requestId}] Undeploying workflow: ${id}`)
|
||||||
|
|
||||||
const { error, workflow: workflowData } = await validateWorkflowPermissions(
|
const {
|
||||||
id,
|
error,
|
||||||
requestId,
|
session,
|
||||||
'admin'
|
workflow: workflowData,
|
||||||
)
|
} = await validateWorkflowPermissions(id, requestId, 'admin')
|
||||||
if (error) {
|
if (error) {
|
||||||
return createErrorResponse(error.message, error.status)
|
return createErrorResponse(error.message, error.status)
|
||||||
}
|
}
|
||||||
@@ -325,6 +339,19 @@ export async function DELETE(
|
|||||||
// Silently fail
|
// Silently fail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workflowData?.workspaceId || null,
|
||||||
|
actorId: session!.user.id,
|
||||||
|
actorName: session?.user?.name,
|
||||||
|
actorEmail: session?.user?.email,
|
||||||
|
action: AuditAction.WORKFLOW_UNDEPLOYED,
|
||||||
|
resourceType: AuditResourceType.WORKFLOW,
|
||||||
|
resourceId: id,
|
||||||
|
resourceName: workflowData?.name,
|
||||||
|
description: `Undeployed workflow "${workflowData?.name || id}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
isDeployed: false,
|
isDeployed: false,
|
||||||
deployedAt: null,
|
deployedAt: null,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
||||||
@@ -22,7 +23,11 @@ export async function POST(
|
|||||||
const { id, version } = await params
|
const { id, version } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error } = await validateWorkflowPermissions(id, requestId, 'admin')
|
const {
|
||||||
|
error,
|
||||||
|
session,
|
||||||
|
workflow: workflowRecord,
|
||||||
|
} = await validateWorkflowPermissions(id, requestId, 'admin')
|
||||||
if (error) {
|
if (error) {
|
||||||
return createErrorResponse(error.message, error.status)
|
return createErrorResponse(error.message, error.status)
|
||||||
}
|
}
|
||||||
@@ -107,6 +112,19 @@ export async function POST(
|
|||||||
logger.error('Error sending workflow reverted event to socket server', e)
|
logger.error('Error sending workflow reverted event to socket server', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workflowRecord?.workspaceId ?? null,
|
||||||
|
actorId: session!.user.id,
|
||||||
|
action: AuditAction.WORKFLOW_DEPLOYMENT_REVERTED,
|
||||||
|
resourceType: AuditResourceType.WORKFLOW,
|
||||||
|
resourceId: id,
|
||||||
|
actorName: session!.user.name ?? undefined,
|
||||||
|
actorEmail: session!.user.email ?? undefined,
|
||||||
|
resourceName: workflowRecord?.name ?? undefined,
|
||||||
|
description: `Reverted workflow to deployment version ${version}`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
message: 'Reverted to deployment version',
|
message: 'Reverted to deployment version',
|
||||||
lastSaved: Date.now(),
|
lastSaved: Date.now(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
||||||
import { restorePreviousVersionWebhooks, saveTriggerWebhooksForDeploy } from '@/lib/webhooks/deploy'
|
import { restorePreviousVersionWebhooks, saveTriggerWebhooksForDeploy } from '@/lib/webhooks/deploy'
|
||||||
@@ -297,6 +298,19 @@ export async function PATCH(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workflowData?.workspaceId,
|
||||||
|
actorId: actorUserId,
|
||||||
|
actorName: session?.user?.name,
|
||||||
|
actorEmail: session?.user?.email,
|
||||||
|
action: AuditAction.WORKFLOW_DEPLOYMENT_ACTIVATED,
|
||||||
|
resourceType: AuditResourceType.WORKFLOW,
|
||||||
|
resourceId: id,
|
||||||
|
description: `Activated deployment version ${versionNum}`,
|
||||||
|
metadata: { version: versionNum },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
success: true,
|
success: true,
|
||||||
deployedAt: result.deployedAt,
|
deployedAt: result.deployedAt,
|
||||||
|
|||||||
@@ -1,6 +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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
@@ -61,6 +62,20 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
`[${requestId}] Successfully duplicated workflow ${sourceWorkflowId} to ${result.id} in ${elapsed}ms`
|
`[${requestId}] Successfully duplicated workflow ${sourceWorkflowId} to ${result.id} in ${elapsed}ms`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workspaceId || null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.WORKFLOW_DUPLICATED,
|
||||||
|
resourceType: AuditResourceType.WORKFLOW,
|
||||||
|
resourceId: result.id,
|
||||||
|
resourceName: result.name,
|
||||||
|
description: `Duplicated workflow from ${sourceWorkflowId}`,
|
||||||
|
metadata: { sourceWorkflowId },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json(result, { status: 201 })
|
return NextResponse.json(result, { status: 201 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loggerMock, setupGlobalFetchMock } from '@sim/testing'
|
import { auditMock, loggerMock, setupGlobalFetchMock } from '@sim/testing'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ vi.mock('@/lib/auth', () => ({
|
|||||||
|
|
||||||
vi.mock('@sim/logger', () => loggerMock)
|
vi.mock('@sim/logger', () => loggerMock)
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
vi.mock('@/lib/workflows/persistence/utils', () => ({
|
vi.mock('@/lib/workflows/persistence/utils', () => ({
|
||||||
loadWorkflowFromNormalizedTables: (workflowId: string) =>
|
loadWorkflowFromNormalizedTables: (workflowId: string) =>
|
||||||
mockLoadWorkflowFromNormalizedTables(workflowId),
|
mockLoadWorkflowFromNormalizedTables(workflowId),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
@@ -336,6 +337,19 @@ export async function DELETE(
|
|||||||
// Don't fail the deletion if Socket.IO notification fails
|
// Don't fail the deletion if Socket.IO notification fails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workflowData.workspaceId || null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.WORKFLOW_DELETED,
|
||||||
|
resourceType: AuditResourceType.WORKFLOW,
|
||||||
|
resourceId: workflowId,
|
||||||
|
resourceName: workflowData.name,
|
||||||
|
description: `Deleted workflow "${workflowData.name}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 })
|
return NextResponse.json({ success: true }, { status: 200 })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const elapsed = Date.now() - startTime
|
const elapsed = Date.now() - startTime
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* @vitest-environment node
|
* @vitest-environment node
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
auditMock,
|
||||||
databaseMock,
|
databaseMock,
|
||||||
defaultMockUser,
|
defaultMockUser,
|
||||||
mockAuth,
|
mockAuth,
|
||||||
@@ -27,6 +28,8 @@ describe('Workflow Variables API Route', () => {
|
|||||||
|
|
||||||
vi.doMock('@sim/db', () => databaseMock)
|
vi.doMock('@sim/db', () => databaseMock)
|
||||||
|
|
||||||
|
vi.doMock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
vi.doMock('@/lib/workflows/utils', () => ({
|
vi.doMock('@/lib/workflows/utils', () => ({
|
||||||
authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission,
|
authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } 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 { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||||
@@ -79,6 +80,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
})
|
})
|
||||||
.where(eq(workflow.id, workflowId))
|
.where(eq(workflow.id, workflowId))
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: workflowData.workspaceId ?? null,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.WORKFLOW_VARIABLES_UPDATED,
|
||||||
|
resourceType: AuditResourceType.WORKFLOW,
|
||||||
|
resourceId: workflowId,
|
||||||
|
resourceName: workflowData.name ?? undefined,
|
||||||
|
description: `Updated workflow variables`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (validationError) {
|
} catch (validationError) {
|
||||||
if (validationError instanceof z.ZodError) {
|
if (validationError instanceof z.ZodError) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, asc, eq, inArray, isNull, min } from 'drizzle-orm'
|
import { and, asc, eq, inArray, isNull, min } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } 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 { getUserEntityPermissions, workspaceExists } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions, workspaceExists } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -188,6 +189,20 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Successfully created empty workflow ${workflowId}`)
|
logger.info(`[${requestId}] Successfully created empty workflow ${workflowId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: auth.userName,
|
||||||
|
actorEmail: auth.userEmail,
|
||||||
|
action: AuditAction.WORKFLOW_CREATED,
|
||||||
|
resourceType: AuditResourceType.WORKFLOW,
|
||||||
|
resourceId: workflowId,
|
||||||
|
resourceName: name,
|
||||||
|
description: `Created workflow "${name}"`,
|
||||||
|
metadata: { name },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id: workflowId,
|
id: workflowId,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq, not } from 'drizzle-orm'
|
import { and, eq, not } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -86,6 +87,19 @@ export async function PUT(
|
|||||||
updatedAt: apiKey.updatedAt,
|
updatedAt: apiKey.updatedAt,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
action: AuditAction.API_KEY_UPDATED,
|
||||||
|
resourceType: AuditResourceType.API_KEY,
|
||||||
|
resourceId: keyId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: name,
|
||||||
|
description: `Updated workspace API key: ${name}`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
logger.info(`[${requestId}] Updated workspace API key: ${keyId} in workspace ${workspaceId}`)
|
logger.info(`[${requestId}] Updated workspace API key: ${keyId} in workspace ${workspaceId}`)
|
||||||
return NextResponse.json({ key: updatedKey })
|
return NextResponse.json({ key: updatedKey })
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -123,12 +137,27 @@ export async function DELETE(
|
|||||||
.where(
|
.where(
|
||||||
and(eq(apiKey.workspaceId, workspaceId), eq(apiKey.id, keyId), eq(apiKey.type, 'workspace'))
|
and(eq(apiKey.workspaceId, workspaceId), eq(apiKey.id, keyId), eq(apiKey.type, 'workspace'))
|
||||||
)
|
)
|
||||||
.returning({ id: apiKey.id })
|
.returning({ id: apiKey.id, name: apiKey.name })
|
||||||
|
|
||||||
if (deletedRows.length === 0) {
|
if (deletedRows.length === 0) {
|
||||||
return NextResponse.json({ error: 'API key not found' }, { status: 404 })
|
return NextResponse.json({ error: 'API key not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deletedKey = deletedRows[0]
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
action: AuditAction.API_KEY_REVOKED,
|
||||||
|
resourceType: AuditResourceType.API_KEY,
|
||||||
|
resourceId: keyId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: deletedKey.name,
|
||||||
|
description: `Revoked workspace API key: ${deletedKey.name}`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
logger.info(`[${requestId}] Deleted workspace API key: ${keyId} from workspace ${workspaceId}`)
|
logger.info(`[${requestId}] Deleted workspace API key: ${keyId} from workspace ${workspaceId}`)
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { nanoid } from 'nanoid'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth'
|
import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
@@ -159,6 +160,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Created workspace API key: ${name} in workspace ${workspaceId}`)
|
logger.info(`[${requestId}] Created workspace API key: ${name} in workspace ${workspaceId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: session?.user?.name,
|
||||||
|
actorEmail: session?.user?.email,
|
||||||
|
action: AuditAction.API_KEY_CREATED,
|
||||||
|
resourceType: AuditResourceType.API_KEY,
|
||||||
|
resourceId: newKey.id,
|
||||||
|
resourceName: name,
|
||||||
|
description: `Created API key "${name}"`,
|
||||||
|
metadata: { keyName: name },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
key: {
|
key: {
|
||||||
...newKey,
|
...newKey,
|
||||||
@@ -222,6 +237,19 @@ export async function DELETE(
|
|||||||
logger.info(
|
logger.info(
|
||||||
`[${requestId}] Deleted ${deletedCount} workspace API keys from workspace ${workspaceId}`
|
`[${requestId}] Deleted ${deletedCount} workspace API keys from workspace ${workspaceId}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: session?.user?.name,
|
||||||
|
actorEmail: session?.user?.email,
|
||||||
|
action: AuditAction.API_KEY_REVOKED,
|
||||||
|
resourceType: AuditResourceType.API_KEY,
|
||||||
|
description: `Revoked ${deletedCount} API key(s)`,
|
||||||
|
metadata: { keyIds: keys, deletedCount },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true, deletedCount })
|
return NextResponse.json({ success: true, deletedCount })
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error(`[${requestId}] Workspace API key DELETE error`, error)
|
logger.error(`[${requestId}] Workspace API key DELETE error`, error)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
|
|||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
@@ -185,6 +186,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Created BYOK key for ${providerId} in workspace ${workspaceId}`)
|
logger.info(`[${requestId}] Created BYOK key for ${providerId} in workspace ${workspaceId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: session?.user?.name,
|
||||||
|
actorEmail: session?.user?.email,
|
||||||
|
action: AuditAction.BYOK_KEY_CREATED,
|
||||||
|
resourceType: AuditResourceType.BYOK_KEY,
|
||||||
|
resourceId: newKey.id,
|
||||||
|
resourceName: providerId,
|
||||||
|
description: `Added BYOK key for ${providerId}`,
|
||||||
|
metadata: { providerId },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
key: {
|
key: {
|
||||||
@@ -242,6 +257,19 @@ export async function DELETE(
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Deleted BYOK key for ${providerId} from workspace ${workspaceId}`)
|
logger.info(`[${requestId}] Deleted BYOK key for ${providerId} from workspace ${workspaceId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: session?.user?.name,
|
||||||
|
actorEmail: session?.user?.email,
|
||||||
|
action: AuditAction.BYOK_KEY_DELETED,
|
||||||
|
resourceType: AuditResourceType.BYOK_KEY,
|
||||||
|
resourceName: providerId,
|
||||||
|
description: `Removed BYOK key for ${providerId}`,
|
||||||
|
metadata: { providerId },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error(`[${requestId}] BYOK key DELETE error`, error)
|
logger.error(`[${requestId}] BYOK key DELETE error`, error)
|
||||||
|
|||||||
@@ -1,6 +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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { duplicateWorkspace } from '@/lib/workspaces/duplicate'
|
import { duplicateWorkspace } from '@/lib/workspaces/duplicate'
|
||||||
@@ -45,6 +46,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
`[${requestId}] Successfully duplicated workspace ${sourceWorkspaceId} to ${result.id} in ${elapsed}ms`
|
`[${requestId}] Successfully duplicated workspace ${sourceWorkspaceId} to ${result.id} in ${elapsed}ms`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: sourceWorkspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.WORKSPACE_DUPLICATED,
|
||||||
|
resourceType: AuditResourceType.WORKSPACE,
|
||||||
|
resourceId: result.id,
|
||||||
|
resourceName: name,
|
||||||
|
description: `Duplicated workspace to "${name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json(result, { status: 201 })
|
return NextResponse.json(result, { status: 201 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
@@ -156,6 +157,19 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
set: { variables: merged, updatedAt: new Date() },
|
set: { variables: merged, updatedAt: new Date() },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: userId,
|
||||||
|
actorName: session?.user?.name,
|
||||||
|
actorEmail: session?.user?.email,
|
||||||
|
action: AuditAction.ENVIRONMENT_UPDATED,
|
||||||
|
resourceType: AuditResourceType.ENVIRONMENT,
|
||||||
|
resourceId: workspaceId,
|
||||||
|
description: `Updated environment variables`,
|
||||||
|
metadata: { keysUpdated: Object.keys(variables) },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`[${requestId}] Workspace env PUT error`, error)
|
logger.error(`[${requestId}] Workspace env PUT error`, error)
|
||||||
|
|||||||
@@ -1,5 +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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { deleteWorkspaceFile } from '@/lib/uploads/contexts/workspace'
|
import { deleteWorkspaceFile } from '@/lib/uploads/contexts/workspace'
|
||||||
@@ -39,6 +40,18 @@ export async function DELETE(
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Deleted workspace file: ${fileId}`)
|
logger.info(`[${requestId}] Deleted workspace file: ${fileId}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.FILE_DELETED,
|
||||||
|
resourceType: AuditResourceType.FILE,
|
||||||
|
resourceId: fileId,
|
||||||
|
description: `Deleted file "${fileId}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { listWorkspaceFiles, uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace'
|
import { listWorkspaceFiles, uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace'
|
||||||
@@ -104,6 +105,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Uploaded workspace file: ${file.name}`)
|
logger.info(`[${requestId}] Uploaded workspace file: ${file.name}`)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.FILE_UPLOADED,
|
||||||
|
resourceType: AuditResourceType.FILE,
|
||||||
|
resourceId: userFile.id,
|
||||||
|
resourceName: file.name,
|
||||||
|
description: `Uploaded file "${file.name}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
file: userFile,
|
file: userFile,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq, inArray } from 'drizzle-orm'
|
import { and, eq, inArray } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -251,6 +252,19 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
|||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.NOTIFICATION_UPDATED,
|
||||||
|
resourceType: AuditResourceType.NOTIFICATION,
|
||||||
|
resourceId: notificationId,
|
||||||
|
resourceName: subscription.notificationType,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Updated ${subscription.notificationType} notification subscription`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
data: {
|
data: {
|
||||||
id: subscription.id,
|
id: subscription.id,
|
||||||
@@ -300,17 +314,35 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|||||||
eq(workspaceNotificationSubscription.workspaceId, workspaceId)
|
eq(workspaceNotificationSubscription.workspaceId, workspaceId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.returning({ id: workspaceNotificationSubscription.id })
|
.returning({
|
||||||
|
id: workspaceNotificationSubscription.id,
|
||||||
|
notificationType: workspaceNotificationSubscription.notificationType,
|
||||||
|
})
|
||||||
|
|
||||||
if (deleted.length === 0) {
|
if (deleted.length === 0) {
|
||||||
return NextResponse.json({ error: 'Notification not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Notification not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deletedSubscription = deleted[0]
|
||||||
|
|
||||||
logger.info('Deleted notification subscription', {
|
logger.info('Deleted notification subscription', {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
subscriptionId: notificationId,
|
subscriptionId: notificationId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.NOTIFICATION_DELETED,
|
||||||
|
resourceType: AuditResourceType.NOTIFICATION,
|
||||||
|
resourceId: notificationId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: deletedSubscription.notificationType,
|
||||||
|
description: `Deleted ${deletedSubscription.notificationType} notification subscription`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error deleting notification', { error })
|
logger.error('Error deleting notification', { error })
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { and, eq, inArray } from 'drizzle-orm'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -256,6 +257,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
type: data.notificationType,
|
type: data.notificationType,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.NOTIFICATION_CREATED,
|
||||||
|
resourceType: AuditResourceType.NOTIFICATION,
|
||||||
|
resourceId: subscription.id,
|
||||||
|
resourceName: data.notificationType,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Created ${data.notificationType} notification subscription`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
data: {
|
data: {
|
||||||
id: subscription.id,
|
id: subscription.id,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import {
|
import {
|
||||||
getUsersWithPermissions,
|
getUsersWithPermissions,
|
||||||
@@ -156,6 +157,21 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
|
|
||||||
const updatedUsers = await getUsersWithPermissions(workspaceId)
|
const updatedUsers = await getUsersWithPermissions(workspaceId)
|
||||||
|
|
||||||
|
for (const update of body.updates) {
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.MEMBER_ROLE_CHANGED,
|
||||||
|
resourceType: AuditResourceType.WORKSPACE,
|
||||||
|
resourceId: workspaceId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Changed permissions for user ${update.userId} to ${update.permissions}`,
|
||||||
|
metadata: { targetUserId: update.userId, newPermissions: update.permissions },
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: 'Permissions updated successfully',
|
message: 'Permissions updated successfully',
|
||||||
users: updatedUsers,
|
users: updatedUsers,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq, inArray } from 'drizzle-orm'
|
import { and, eq, inArray } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceByIdAPI')
|
const logger = createLogger('WorkspaceByIdAPI')
|
||||||
@@ -228,6 +229,13 @@ export async function DELETE(
|
|||||||
`Deleting workspace ${workspaceId} for user ${session.user.id}, deleteTemplates: ${deleteTemplates}`
|
`Deleting workspace ${workspaceId} for user ${session.user.id}, deleteTemplates: ${deleteTemplates}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Fetch workspace name before deletion for audit logging
|
||||||
|
const [workspaceRecord] = await db
|
||||||
|
.select({ name: workspace.name })
|
||||||
|
.from(workspace)
|
||||||
|
.where(eq(workspace.id, workspaceId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
// Delete workspace and all related data in a transaction
|
// Delete workspace and all related data in a transaction
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
// Get all workflows in this workspace before deletion
|
// Get all workflows in this workspace before deletion
|
||||||
@@ -281,6 +289,19 @@ export async function DELETE(
|
|||||||
logger.info(`Successfully deleted workspace ${workspaceId} and all related data`)
|
logger.info(`Successfully deleted workspace ${workspaceId} and all related data`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: null,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.WORKSPACE_DELETED,
|
||||||
|
resourceType: AuditResourceType.WORKSPACE,
|
||||||
|
resourceId: workspaceId,
|
||||||
|
resourceName: workspaceRecord?.name,
|
||||||
|
description: `Deleted workspace "${workspaceRecord?.name || workspaceId}"`,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting workspace ${workspaceId}:`, error)
|
logger.error(`Error deleting workspace ${workspaceId}:`, error)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSession, createWorkspaceRecord, loggerMock } from '@sim/testing'
|
import { auditMock, createSession, createWorkspaceRecord, loggerMock } from '@sim/testing'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
@@ -55,6 +55,8 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({
|
|||||||
|
|
||||||
vi.mock('@sim/logger', () => loggerMock)
|
vi.mock('@sim/logger', () => loggerMock)
|
||||||
|
|
||||||
|
vi.mock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
vi.mock('@/lib/core/utils/urls', () => ({
|
vi.mock('@/lib/core/utils/urls', () => ({
|
||||||
getBaseUrl: vi.fn().mockReturnValue('https://test.sim.ai'),
|
getBaseUrl: vi.fn().mockReturnValue('https://test.sim.ai'),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { WorkspaceInvitationEmail } from '@/components/emails'
|
import { WorkspaceInvitationEmail } from '@/components/emails'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||||
@@ -162,6 +163,19 @@ export async function GET(
|
|||||||
.where(eq(workspaceInvitation.id, invitation.id))
|
.where(eq(workspaceInvitation.id, invitation.id))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: invitation.workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.INVITATION_ACCEPTED,
|
||||||
|
resourceType: AuditResourceType.WORKSPACE,
|
||||||
|
resourceId: invitation.workspaceId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
resourceName: workspaceDetails.name,
|
||||||
|
description: `Accepted workspace invitation to "${workspaceDetails.name}"`,
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.redirect(new URL(`/workspace/${invitation.workspaceId}/w`, getBaseUrl()))
|
return NextResponse.redirect(new URL(`/workspace/${invitation.workspaceId}/w`, getBaseUrl()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +230,19 @@ export async function DELETE(
|
|||||||
|
|
||||||
await db.delete(workspaceInvitation).where(eq(workspaceInvitation.id, invitationId))
|
await db.delete(workspaceInvitation).where(eq(workspaceInvitation.id, invitationId))
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: invitation.workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
action: AuditAction.INVITATION_REVOKED,
|
||||||
|
resourceType: AuditResourceType.WORKSPACE,
|
||||||
|
resourceId: invitation.workspaceId,
|
||||||
|
actorName: session.user.name ?? undefined,
|
||||||
|
actorEmail: session.user.email ?? undefined,
|
||||||
|
description: `Revoked workspace invitation for ${invitation.email}`,
|
||||||
|
metadata: { invitationId, email: invitation.email },
|
||||||
|
request: _request,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error deleting workspace invitation:', error)
|
logger.error('Error deleting workspace invitation:', error)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createMockRequest, mockAuth, mockConsoleLogger } from '@sim/testing'
|
import { auditMock, createMockRequest, mockAuth, mockConsoleLogger } from '@sim/testing'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
describe('Workspace Invitations API Route', () => {
|
describe('Workspace Invitations API Route', () => {
|
||||||
@@ -96,6 +96,8 @@ describe('Workspace Invitations API Route', () => {
|
|||||||
getEmailDomain: vi.fn().mockReturnValue('sim.ai'),
|
getEmailDomain: vi.fn().mockReturnValue('sim.ai'),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.doMock('@/lib/audit/log', () => auditMock)
|
||||||
|
|
||||||
vi.doMock('drizzle-orm', () => ({
|
vi.doMock('drizzle-orm', () => ({
|
||||||
and: vi.fn().mockImplementation((...args) => ({ type: 'and', conditions: args })),
|
and: vi.fn().mockImplementation((...args) => ({ type: 'and', conditions: args })),
|
||||||
eq: vi.fn().mockImplementation((field, value) => ({ type: 'eq', field, value })),
|
eq: vi.fn().mockImplementation((field, value) => ({ type: 'eq', field, value })),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq, inArray } from 'drizzle-orm'
|
import { and, eq, inArray } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { WorkspaceInvitationEmail } from '@/components/emails'
|
import { WorkspaceInvitationEmail } from '@/components/emails'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
@@ -214,6 +215,20 @@ export async function POST(req: NextRequest) {
|
|||||||
token: token,
|
token: token,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.MEMBER_INVITED,
|
||||||
|
resourceType: AuditResourceType.WORKSPACE,
|
||||||
|
resourceId: workspaceId,
|
||||||
|
resourceName: email,
|
||||||
|
description: `Invited ${email} as ${permission}`,
|
||||||
|
metadata: { email, role: permission },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true, invitation: invitationData })
|
return NextResponse.json({ success: true, invitation: invitationData })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof InvitationsNotAllowedError) {
|
if (error instanceof InvitationsNotAllowedError) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } 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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
|
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
@@ -101,6 +102,19 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.MEMBER_REMOVED,
|
||||||
|
resourceType: AuditResourceType.WORKSPACE,
|
||||||
|
resourceId: workspaceId,
|
||||||
|
description: isSelf ? 'Left the workspace' : 'Removed a member from the workspace',
|
||||||
|
metadata: { removedUserId: userId, selfRemoval: isSelf },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error removing workspace member:', error)
|
logger.error('Error removing workspace member:', error)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, desc, eq, isNull } from 'drizzle-orm'
|
import { and, desc, eq, isNull } from 'drizzle-orm'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
||||||
@@ -68,6 +69,20 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow)
|
const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow)
|
||||||
|
|
||||||
|
recordAudit({
|
||||||
|
workspaceId: newWorkspace.id,
|
||||||
|
actorId: session.user.id,
|
||||||
|
actorName: session.user.name,
|
||||||
|
actorEmail: session.user.email,
|
||||||
|
action: AuditAction.WORKSPACE_CREATED,
|
||||||
|
resourceType: AuditResourceType.WORKSPACE,
|
||||||
|
resourceId: newWorkspace.id,
|
||||||
|
resourceName: newWorkspace.name,
|
||||||
|
description: `Created workspace "${newWorkspace.name}"`,
|
||||||
|
metadata: { name: newWorkspace.name },
|
||||||
|
request: req,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ workspace: newWorkspace })
|
return NextResponse.json({ workspace: newWorkspace })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error creating workspace:', error)
|
logger.error('Error creating workspace:', error)
|
||||||
|
|||||||
@@ -30,21 +30,6 @@ export const ChatMessageContainer = memo(function ChatMessageContainer({
|
|||||||
}: ChatMessageContainerProps) {
|
}: ChatMessageContainerProps) {
|
||||||
return (
|
return (
|
||||||
<div className='relative flex flex-1 flex-col overflow-hidden bg-white'>
|
<div className='relative flex flex-1 flex-col overflow-hidden bg-white'>
|
||||||
<style jsx>{`
|
|
||||||
@keyframes growShrink {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.loading-dot {
|
|
||||||
animation: growShrink 1.5s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
{/* Scrollable Messages Area */}
|
{/* Scrollable Messages Area */}
|
||||||
<div
|
<div
|
||||||
ref={messagesContainerRef}
|
ref={messagesContainerRef}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export function VoiceInterface({
|
|||||||
const [state, setState] = useState<'idle' | 'listening' | 'agent_speaking'>('idle')
|
const [state, setState] = useState<'idle' | 'listening' | 'agent_speaking'>('idle')
|
||||||
const [isInitialized, setIsInitialized] = useState(false)
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
const [isMuted, setIsMuted] = useState(false)
|
const [isMuted, setIsMuted] = useState(false)
|
||||||
const [audioLevels, setAudioLevels] = useState<number[]>(new Array(200).fill(0))
|
const [audioLevels, setAudioLevels] = useState<number[]>(() => new Array(200).fill(0))
|
||||||
const [permissionStatus, setPermissionStatus] = useState<'prompt' | 'granted' | 'denied'>(
|
const [permissionStatus, setPermissionStatus] = useState<'prompt' | 'granted' | 'denied'>(
|
||||||
'prompt'
|
'prompt'
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user