diff --git a/app/api/settings/environment/route.ts b/app/api/settings/environment/route.ts new file mode 100644 index 0000000000..4dd1034d61 --- /dev/null +++ b/app/api/settings/environment/route.ts @@ -0,0 +1,88 @@ +import { NextResponse } from 'next/server' +import { eq } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { z } from 'zod' +import { hashSecret } from '@/lib/utils' +import { EnvironmentVariable } from '@/stores/settings/environment/types' +import { db } from '@/db' +import { userEnvironment } from '@/db/schema' + +const EnvironmentSchema = z.object({ + userId: z.string(), + data: z.string(), // A JSON stringified object of envvars +}) + +export async function POST(request: Request) { + try { + const body = await request.json() + const { userId, data } = EnvironmentSchema.parse(body) + + // Parse the incoming JSON string + const parsedData = JSON.parse(data) as Record + + // Hash all environment variables with unique salts + const securedData = await Promise.all( + Object.entries(parsedData).map(async ([key, value]) => { + const { hash, salt } = await hashSecret(value.value) + return [key, { key, value: hash, salt }] + }) + ) + + // Store the hashed values + await db + .insert(userEnvironment) + .values({ + id: nanoid(), + userId, + variables: JSON.stringify(Object.fromEntries(securedData)), + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: [userEnvironment.userId], + set: { + variables: JSON.stringify(Object.fromEntries(securedData)), + updatedAt: new Date(), + }, + }) + + return NextResponse.json({ success: true }, { status: 200 }) + } catch (error: any) { + console.error('Environment update error:', error) + return NextResponse.json({ error: error.message }, { status: 500 }) + } +} + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json({ error: 'userId is required' }, { status: 400 }) + } + + const result = await db + .select() + .from(userEnvironment) + .where(eq(userEnvironment.userId, userId)) + .limit(1) + + if (!result.length) { + return NextResponse.json({ data: {} }, { status: 200 }) + } + + // Parse the variables and return just the structure without the hashed values + const variables = JSON.parse(result[0].variables) + const sanitizedVariables = Object.fromEntries( + Object.entries(variables).map(([key, value]: [string, any]) => [ + key, + { key, value: '••••••••' }, // Hide the actual value + ]) + ) + + return NextResponse.json({ data: sanitizedVariables }, { status: 200 }) + } catch (error: any) { + console.error('Environment fetch error:', error) + return NextResponse.json({ error: error.message }, { status: 500 }) + } +} diff --git a/app/api/settings/general/route.ts b/app/api/settings/general/route.ts new file mode 100644 index 0000000000..7a28dcf1b5 --- /dev/null +++ b/app/api/settings/general/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from 'next/server' +import { eq } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { z } from 'zod' +import { db } from '@/db' +import { userSettings } from '@/db/schema' + +const SettingsSchema = z.object({ + userId: z.string(), + isAutoConnectEnabled: z.boolean().default(true), +}) + +export async function POST(request: Request) { + try { + const body = await request.json() + const { userId, isAutoConnectEnabled } = SettingsSchema.parse(body) + + // Store the settings + await db + .insert(userSettings) + .values({ + id: nanoid(), + userId, + isAutoConnectEnabled, + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: [userSettings.userId], + set: { + isAutoConnectEnabled, + updatedAt: new Date(), + }, + }) + + return NextResponse.json({ success: true }, { status: 200 }) + } catch (error: any) { + console.error('Settings update error:', error) + return NextResponse.json({ error: error.message }, { status: 500 }) + } +} + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json({ error: 'userId is required' }, { status: 400 }) + } + + const result = await db + .select() + .from(userSettings) + .where(eq(userSettings.userId, userId)) + .limit(1) + + if (!result.length) { + return NextResponse.json( + { + data: { + isAutoConnectEnabled: true, // Return default values + }, + }, + { status: 200 } + ) + } + + return NextResponse.json( + { + data: { + isAutoConnectEnabled: result[0].isAutoConnectEnabled, + }, + }, + { status: 200 } + ) + } catch (error: any) { + console.error('Settings fetch error:', error) + return NextResponse.json({ error: error.message }, { status: 500 }) + } +} diff --git a/app/w/[id]/workflow.tsx b/app/w/[id]/workflow.tsx index 5944c2ad02..224a2534e8 100644 --- a/app/w/[id]/workflow.tsx +++ b/app/w/[id]/workflow.tsx @@ -19,7 +19,6 @@ import { initializeStateLogger } from '@/stores/workflow/logger' import { useWorkflowRegistry } from '@/stores/workflow/registry/store' import { useWorkflowStore } from '@/stores/workflow/store' import { NotificationList } from '@/app/w/components/notifications/notifications' -import { WorkflowSyncWrapper } from '@/app/w/components/workflows/sync-wrapper' import { getBlock } from '../../../blocks' import { ErrorBoundary } from '../components/error-boundary/error-boundary' import { CustomEdge } from './components/custom-edge/custom-edge' @@ -379,9 +378,7 @@ export default function Workflow() { return ( - - - + ) diff --git a/app/w/components/workflows/sync-wrapper.tsx b/app/w/components/workflows/sync-wrapper.tsx deleted file mode 100644 index 0428922e52..0000000000 --- a/app/w/components/workflows/sync-wrapper.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from 'react' -import { - useDebouncedWorkflowSync, - usePeriodicWorkflowSync, - useSyncOnUnload, -} from '@/stores/workflow/sync/hooks' - -interface WorkflowSyncWrapperProps { - children: ReactNode -} - -export function WorkflowSyncWrapper({ children }: WorkflowSyncWrapperProps) { - useDebouncedWorkflowSync() - usePeriodicWorkflowSync() - useSyncOnUnload() - - return <>{children} -} diff --git a/db/migrations/0003_smiling_hammerhead.sql b/db/migrations/0003_smiling_hammerhead.sql new file mode 100644 index 0000000000..4b05884dec --- /dev/null +++ b/db/migrations/0003_smiling_hammerhead.sql @@ -0,0 +1,28 @@ +CREATE TABLE "logs" ( + "id" text PRIMARY KEY NOT NULL, + "workflow_id" text NOT NULL, + "execution_id" text, + "level" text NOT NULL, + "message" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user_environment" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "variables" text NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "user_environment_user_id_unique" UNIQUE("user_id") +); +--> statement-breakpoint +CREATE TABLE "user_settings" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "is_auto_connect_enabled" boolean DEFAULT true NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "user_settings_user_id_unique" UNIQUE("user_id") +); +--> statement-breakpoint +ALTER TABLE "logs" ADD CONSTRAINT "logs_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_environment" ADD CONSTRAINT "user_environment_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/db/migrations/0004_nasty_mesmero.sql b/db/migrations/0004_nasty_mesmero.sql new file mode 100644 index 0000000000..17c59bf572 --- /dev/null +++ b/db/migrations/0004_nasty_mesmero.sql @@ -0,0 +1 @@ +ALTER TABLE "user_settings" ADD COLUMN "is_debug_mode_enabled" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/db/migrations/meta/0003_snapshot.json b/db/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000000..69671a0b06 --- /dev/null +++ b/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,599 @@ +{ + "id": "3ee25aee-2e62-4bf0-bb64-dc01bf8cad70", + "prevId": "80542c3c-48a3-41e5-b911-fe51152efccc", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.logs": { + "name": "logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "logs_workflow_id_workflow_id_fk": { + "name": "logs_workflow_id_workflow_id_fk", + "tableFrom": "logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_environment": { + "name": "user_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_environment_user_id_user_id_fk": { + "name": "user_environment_user_id_user_id_fk", + "tableFrom": "user_environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_environment_user_id_unique": { + "name": "user_environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_auto_connect_enabled": { + "name": "is_auto_connect_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_settings_user_id_user_id_fk": { + "name": "user_settings_user_id_user_id_fk", + "tableFrom": "user_settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_settings_user_id_unique": { + "name": "user_settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/db/migrations/meta/0004_snapshot.json b/db/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000000..84b043f7a3 --- /dev/null +++ b/db/migrations/meta/0004_snapshot.json @@ -0,0 +1,606 @@ +{ + "id": "1efb46d6-313c-4fc9-8393-b90a990bfcc5", + "prevId": "3ee25aee-2e62-4bf0-bb64-dc01bf8cad70", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.logs": { + "name": "logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "logs_workflow_id_workflow_id_fk": { + "name": "logs_workflow_id_workflow_id_fk", + "tableFrom": "logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_environment": { + "name": "user_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_environment_user_id_user_id_fk": { + "name": "user_environment_user_id_user_id_fk", + "tableFrom": "user_environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_environment_user_id_unique": { + "name": "user_environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_auto_connect_enabled": { + "name": "is_auto_connect_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_debug_mode_enabled": { + "name": "is_debug_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_settings_user_id_user_id_fk": { + "name": "user_settings_user_id_user_id_fk", + "tableFrom": "user_settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_settings_user_id_unique": { + "name": "user_settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index e3bfcbdf53..bf7ede9c67 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -22,6 +22,20 @@ "when": 1739776634326, "tag": "0002_previous_xavin", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1739818168546, + "tag": "0003_smiling_hammerhead", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1739819841402, + "tag": "0004_nasty_mesmero", + "breakpoints": true } ] } diff --git a/db/schema.ts b/db/schema.ts index 7b147bb51b..2abe25aa41 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -1,4 +1,4 @@ -import { boolean, integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core' +import { boolean, integer, pgTable, text, timestamp, unique } from 'drizzle-orm/pg-core' export const user = pgTable('user', { id: text('id').primaryKey(), @@ -70,3 +70,35 @@ export const waitlist = pgTable('waitlist', { createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }) + +export const consoleLog = pgTable('logs', { + id: text('id').primaryKey(), + workflowId: text('workflow_id') + .notNull() + .references(() => workflow.id, { onDelete: 'cascade' }), + executionId: text('execution_id'), + level: text('level').notNull(), // e.g. "info", "error", etc. + message: text('message').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), +}) + +export const userEnvironment = pgTable('user_environment', { + id: text('id').primaryKey(), // Use the user id as the key + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }) + .unique(), // One environment per user + variables: text('variables').notNull(), // JSON stringified {key: hashedValue} + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}) + +export const userSettings = pgTable('user_settings', { + id: text('id').primaryKey(), // Use the user id as the key + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }) + .unique(), // One settings record per user + isAutoConnectEnabled: boolean('is_auto_connect_enabled').notNull().default(true), + isDebugModeEnabled: boolean('is_debug_mode_enabled').notNull().default(false), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}) diff --git a/lib/utils.ts b/lib/utils.ts index d32b0fe652..aa791787de 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,25 @@ import { type ClassValue, clsx } from 'clsx' +import { createHash } from 'crypto' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +/** + * Hashes a secret using SHA-256 with a salt + * @param secret - The secret to hash + * @param salt - Optional salt to use for hashing. If not provided, a random salt will be generated + * @returns A promise that resolves to an object containing the hashed secret and salt + */ +export async function hashSecret( + secret: string, + salt?: string +): Promise<{ hash: string; salt: string }> { + const useSalt = + salt || createHash('sha256').update(crypto.randomUUID()).digest('hex').slice(0, 16) + const hash = createHash('sha256') + .update(secret + useSalt) + .digest('hex') + return { hash, salt: useSalt } +} diff --git a/middleware.ts b/middleware.ts index b1ef96d4d8..211337f155 100644 --- a/middleware.ts +++ b/middleware.ts @@ -11,5 +11,5 @@ export async function middleware(request: NextRequest) { // TODO: Add protected routes export const config = { - matcher: ['/dashboard/:path*', '/w/:path*'], + matcher: ['/w/:path*'], } diff --git a/stores/index.ts b/stores/index.ts index 9aacab6c85..085a707604 100644 --- a/stores/index.ts +++ b/stores/index.ts @@ -4,9 +4,15 @@ import { useExecutionStore } from './execution/store' import { useNotificationStore } from './notifications/store' import { useEnvironmentStore } from './settings/environment/store' import { useGeneralStore } from './settings/general/store' +import { initializeSyncManager } from './sync-manager' import { useWorkflowRegistry } from './workflow/registry/store' import { useWorkflowStore } from './workflow/store' +// Initialize sync manager when the store is first imported +if (typeof window !== 'undefined') { + initializeSyncManager() +} + // Reset all application stores to their initial state export const resetAllStores = () => { // Selectively clear localStorage items @@ -29,7 +35,7 @@ export const resetAllStores = () => { useEnvironmentStore.setState({ variables: {} }) useExecutionStore.getState().reset() useConsoleStore.setState({ entries: [], isOpen: false }) - useGeneralStore.setState({ isAutoConnectEnabled: true }) + useGeneralStore.setState({ isAutoConnectEnabled: true, isDebugModeEnabled: false }) useChatStore.setState({ messages: [], isProcessing: false, error: null }) } diff --git a/stores/settings/general/store.ts b/stores/settings/general/store.ts index 31b7e3e7d4..c11193b88c 100644 --- a/stores/settings/general/store.ts +++ b/stores/settings/general/store.ts @@ -3,10 +3,12 @@ import { devtools, persist } from 'zustand/middleware' interface General { isAutoConnectEnabled: boolean + isDebugModeEnabled: boolean } interface GeneralActions { toggleAutoConnect: () => void + toggleDebugMode: () => void } type GeneralStore = General & GeneralActions @@ -16,9 +18,10 @@ export const useGeneralStore = create()( persist( (set) => ({ isAutoConnectEnabled: true, - + isDebugModeEnabled: false, toggleAutoConnect: () => set((state) => ({ isAutoConnectEnabled: !state.isAutoConnectEnabled })), + toggleDebugMode: () => set((state) => ({ isDebugModeEnabled: !state.isDebugModeEnabled })), }), { name: 'general-settings', diff --git a/stores/sync-manager.ts b/stores/sync-manager.ts new file mode 100644 index 0000000000..5f44f4563b --- /dev/null +++ b/stores/sync-manager.ts @@ -0,0 +1,70 @@ +import { useWorkflowRegistry } from './workflow/registry/store' +import { useWorkflowStore } from './workflow/store' + +interface SyncPayload { + id: string + name: string + description?: string + state: string +} + +async function syncWorkflowToServer(payload: SyncPayload): Promise { + try { + const response = await fetch('/api/workflows/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + keepalive: true, // Ensures request completes even during page unload + }) + + if (!response.ok) { + if (response.status === 401) { + window.location.href = '/login' + return false + } + throw new Error(`Sync failed: ${response.statusText}`) + } + + console.log('Workflow synced successfully') + return true + } catch (error) { + console.error('Error syncing workflow:', error) + return false + } +} + +export function initializeSyncManager() { + if (typeof window === 'undefined') return + + const handleBeforeUnload = async (event: BeforeUnloadEvent) => { + const { activeWorkflowId, workflows } = useWorkflowRegistry.getState() + const workflowState = useWorkflowStore.getState() + + if (!activeWorkflowId || !workflows[activeWorkflowId]) { + return + } + + const activeWorkflow = workflows[activeWorkflowId] + const payload: SyncPayload = { + id: activeWorkflowId, + name: activeWorkflow.name, + description: activeWorkflow.description, + state: JSON.stringify({ + blocks: workflowState.blocks, + edges: workflowState.edges, + loops: workflowState.loops, + lastSaved: workflowState.lastSaved, + }), + } + + // Show confirmation dialog + event.preventDefault() + event.returnValue = '' + + // Attempt to sync + await syncWorkflowToServer(payload) + } + + window.addEventListener('beforeunload', handleBeforeUnload) + return () => window.removeEventListener('beforeunload', handleBeforeUnload) +} diff --git a/stores/workflow/sync/hooks.ts b/stores/workflow/sync/hooks.ts deleted file mode 100644 index 197ea12f72..0000000000 --- a/stores/workflow/sync/hooks.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { useEffect, useRef } from 'react' -import { useRouter } from 'next/navigation' -import debounce from 'lodash.debounce' -import { useNotificationStore } from '@/stores/notifications/store' -import { useWorkflowRegistry } from '../registry/store' -import { useWorkflowStore } from '../store' - -const SYNC_DEBOUNCE_MS = 2000 // 2 seconds -const PERIODIC_SYNC_MS = 30000 // 30 seconds - -interface SyncPayload { - id: string - name: string - description?: string - state: string -} - -async function syncWorkflowToServer(payload: SyncPayload): Promise { - try { - const response = await fetch('/api/workflows/sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }) - - if (!response.ok) { - if (response.status === 401) { - // Auth error - will be handled by the middleware - window.location.href = '/login' - return false - } - throw new Error(`Sync failed: ${response.statusText}`) - } - - return true - } catch (error) { - console.error('Error syncing workflow:', error) - return false - } -} - -export function useDebouncedWorkflowSync() { - const router = useRouter() - const { addNotification } = useNotificationStore() - const workflowState = useWorkflowStore((state) => ({ - blocks: state.blocks, - edges: state.edges, - loops: state.loops, - lastSaved: state.lastSaved, - })) - const { activeWorkflowId, workflows } = useWorkflowRegistry() - - const debouncedSyncRef = useRef | null>(null) - - useEffect(() => { - if (!activeWorkflowId || !workflows[activeWorkflowId]) return - - const syncWorkflow = async () => { - const activeWorkflow = workflows[activeWorkflowId] - const payload: SyncPayload = { - id: activeWorkflowId, - name: activeWorkflow.name, - description: activeWorkflow.description, - state: JSON.stringify(workflowState), - } - - const success = await syncWorkflowToServer(payload) - if (!success) { - addNotification( - 'error', - 'Failed to save workflow changes. Please try again.', - activeWorkflowId - ) - } - } - - // Create a debounced version of syncWorkflow - if (!debouncedSyncRef.current) { - debouncedSyncRef.current = debounce(syncWorkflow, SYNC_DEBOUNCE_MS) - } - - // Call the debounced sync - debouncedSyncRef.current() - - // Cleanup - return () => { - debouncedSyncRef.current?.cancel() - } - }, [activeWorkflowId, workflows, workflowState, addNotification]) -} - -export function usePeriodicWorkflowSync() { - const { addNotification } = useNotificationStore() - const workflowState = useWorkflowStore((state) => ({ - blocks: state.blocks, - edges: state.edges, - loops: state.loops, - lastSaved: state.lastSaved, - })) - const { activeWorkflowId, workflows } = useWorkflowRegistry() - - useEffect(() => { - if (!activeWorkflowId || !workflows[activeWorkflowId]) return - - const syncWorkflow = async () => { - const activeWorkflow = workflows[activeWorkflowId] - const payload: SyncPayload = { - id: activeWorkflowId, - name: activeWorkflow.name, - description: activeWorkflow.description, - state: JSON.stringify(workflowState), - } - - const success = await syncWorkflowToServer(payload) - if (!success) { - addNotification( - 'error', - 'Failed to auto-save workflow changes. Please save manually.', - activeWorkflowId - ) - } - } - - const intervalId = setInterval(syncWorkflow, PERIODIC_SYNC_MS) - - return () => clearInterval(intervalId) - }, [activeWorkflowId, workflows, workflowState, addNotification]) -} - -export function useSyncOnUnload() { - const { addNotification } = useNotificationStore() - const workflowState = useWorkflowStore((state) => ({ - blocks: state.blocks, - edges: state.edges, - loops: state.loops, - lastSaved: state.lastSaved, - })) - const { activeWorkflowId, workflows } = useWorkflowRegistry() - - useEffect(() => { - if (!activeWorkflowId || !workflows[activeWorkflowId]) return - - const handleBeforeUnload = async (event: BeforeUnloadEvent) => { - const activeWorkflow = workflows[activeWorkflowId] - const payload: SyncPayload = { - id: activeWorkflowId, - name: activeWorkflow.name, - description: activeWorkflow.description, - state: JSON.stringify(workflowState), - } - - // Use the keepalive option to try to complete the request even during unload - const response = await fetch('/api/workflows/sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - keepalive: true, - }) - - if (!response.ok) { - addNotification( - 'error', - 'Failed to save workflow changes before closing.', - activeWorkflowId - ) - } - - // Show a confirmation dialog - event.preventDefault() - event.returnValue = '' - } - - window.addEventListener('beforeunload', handleBeforeUnload) - return () => window.removeEventListener('beforeunload', handleBeforeUnload) - }, [activeWorkflowId, workflows, workflowState, addNotification]) -}