mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(apikeys): pinned api key to track API key a workflow is deployed with (#924)
* fix(apikeys): pinned api key to track API key a workflow is deployed with * remove deprecated behaviour tests
This commit is contained in:
committed by
GitHub
parent
a2040322e7
commit
ebb25469ab
@@ -118,44 +118,49 @@ describe('Workflow Deployment API Route', () => {
|
||||
db: {
|
||||
select: vi.fn().mockImplementation(() => {
|
||||
selectCallCount++
|
||||
const buildLimitResponse = () => ({
|
||||
limit: vi.fn().mockImplementation(() => {
|
||||
// First call: workflow lookup (should return workflow)
|
||||
if (selectCallCount === 1) {
|
||||
return Promise.resolve([{ userId: 'user-id', id: 'workflow-id' }])
|
||||
}
|
||||
// Second call: blocks lookup
|
||||
if (selectCallCount === 2) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: 'block-1',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
positionX: '100',
|
||||
positionY: '100',
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
data: {},
|
||||
},
|
||||
])
|
||||
}
|
||||
// Third call: edges lookup
|
||||
if (selectCallCount === 3) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Fourth call: subflows lookup
|
||||
if (selectCallCount === 4) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Fifth call: API key lookup (should return empty for new key test)
|
||||
if (selectCallCount === 5) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Default: empty array
|
||||
return Promise.resolve([])
|
||||
}),
|
||||
})
|
||||
|
||||
return {
|
||||
from: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockImplementation(() => ({
|
||||
limit: vi.fn().mockImplementation(() => {
|
||||
// First call: workflow lookup (should return workflow)
|
||||
if (selectCallCount === 1) {
|
||||
return Promise.resolve([{ userId: 'user-id', id: 'workflow-id' }])
|
||||
}
|
||||
// Second call: blocks lookup
|
||||
if (selectCallCount === 2) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: 'block-1',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
positionX: '100',
|
||||
positionY: '100',
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
data: {},
|
||||
},
|
||||
])
|
||||
}
|
||||
// Third call: edges lookup
|
||||
if (selectCallCount === 3) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Fourth call: subflows lookup
|
||||
if (selectCallCount === 4) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Fifth call: API key lookup (should return empty for new key test)
|
||||
if (selectCallCount === 5) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Default: empty array
|
||||
return Promise.resolve([])
|
||||
}),
|
||||
...buildLimitResponse(),
|
||||
orderBy: vi.fn().mockReturnValue(buildLimitResponse()),
|
||||
})),
|
||||
})),
|
||||
}
|
||||
@@ -216,160 +221,7 @@ describe('Workflow Deployment API Route', () => {
|
||||
expect(data).toHaveProperty('deployedAt', null)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test POST deployment with no existing API key
|
||||
* This should generate a new API key
|
||||
*/
|
||||
it('should create new API key when deploying workflow for user with no API key', async () => {
|
||||
// Override the global mock for this specific test
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi
|
||||
.fn()
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ userId: 'user-id', id: 'workflow-id' }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'block-1',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
positionX: '100',
|
||||
positionY: '100',
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
data: {},
|
||||
},
|
||||
]),
|
||||
}),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([]), // No edges
|
||||
}),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([]), // No subflows
|
||||
}),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([]), // No existing API key
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockImplementation(() => ({
|
||||
values: vi.fn().mockResolvedValue([{ id: 'mock-api-key-id' }]),
|
||||
})),
|
||||
update: vi.fn().mockImplementation(() => ({
|
||||
set: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST')
|
||||
|
||||
const params = Promise.resolve({ id: 'workflow-id' })
|
||||
|
||||
const { POST } = await import('@/app/api/workflows/[id]/deploy/route')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
expect(data).toHaveProperty('apiKey', 'sim_testkeygenerated12345')
|
||||
expect(data).toHaveProperty('isDeployed', true)
|
||||
expect(data).toHaveProperty('deployedAt')
|
||||
})
|
||||
|
||||
/**
|
||||
* Test POST deployment with existing API key
|
||||
* This should use the existing API key
|
||||
*/
|
||||
it('should use existing API key when deploying workflow', async () => {
|
||||
// Override the global mock for this specific test
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi
|
||||
.fn()
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ userId: 'user-id', id: 'workflow-id' }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'block-1',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
positionX: '100',
|
||||
positionY: '100',
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
data: {},
|
||||
},
|
||||
]),
|
||||
}),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([]), // No edges
|
||||
}),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([]), // No subflows
|
||||
}),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ key: 'sim_existingtestapikey12345' }]), // Existing API key
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockImplementation(() => ({
|
||||
values: vi.fn().mockResolvedValue([{ id: 'mock-api-key-id' }]),
|
||||
})),
|
||||
update: vi.fn().mockImplementation(() => ({
|
||||
set: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST')
|
||||
|
||||
const params = Promise.resolve({ id: 'workflow-id' })
|
||||
|
||||
const { POST } = await import('@/app/api/workflows/[id]/deploy/route')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
expect(data).toHaveProperty('apiKey', 'sim_existingtestapikey12345')
|
||||
expect(data).toHaveProperty('isDeployed', true)
|
||||
})
|
||||
// Removed two POST deployment tests by request
|
||||
|
||||
/**
|
||||
* Test DELETE undeployment
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -33,6 +33,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
deployedAt: workflow.deployedAt,
|
||||
userId: workflow.userId,
|
||||
deployedState: workflow.deployedState,
|
||||
pinnedApiKey: workflow.pinnedApiKey,
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, id))
|
||||
@@ -56,37 +57,42 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch the user's API key
|
||||
const userApiKey = await db
|
||||
.select({
|
||||
key: apiKey.key,
|
||||
})
|
||||
.from(apiKey)
|
||||
.where(eq(apiKey.userId, workflowData.userId))
|
||||
.limit(1)
|
||||
let userKey: string | null = null
|
||||
|
||||
let userKey = null
|
||||
|
||||
// If no API key exists, create one automatically
|
||||
if (userApiKey.length === 0) {
|
||||
try {
|
||||
const newApiKey = generateApiKey()
|
||||
await db.insert(apiKey).values({
|
||||
id: uuidv4(),
|
||||
userId: workflowData.userId,
|
||||
name: 'Default API Key',
|
||||
key: newApiKey,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
userKey = newApiKey
|
||||
logger.info(`[${requestId}] Generated new API key for user: ${workflowData.userId}`)
|
||||
} catch (keyError) {
|
||||
// If key generation fails, log the error but continue with the request
|
||||
logger.error(`[${requestId}] Failed to generate API key:`, keyError)
|
||||
}
|
||||
if (workflowData.pinnedApiKey) {
|
||||
userKey = workflowData.pinnedApiKey
|
||||
} else {
|
||||
userKey = userApiKey[0].key
|
||||
// Fetch the user's API key, preferring the most recently used
|
||||
const userApiKey = await db
|
||||
.select({
|
||||
key: apiKey.key,
|
||||
})
|
||||
.from(apiKey)
|
||||
.where(eq(apiKey.userId, workflowData.userId))
|
||||
.orderBy(desc(apiKey.lastUsed), desc(apiKey.createdAt))
|
||||
.limit(1)
|
||||
|
||||
// If no API key exists, create one automatically
|
||||
if (userApiKey.length === 0) {
|
||||
try {
|
||||
const newApiKeyVal = generateApiKey()
|
||||
await db.insert(apiKey).values({
|
||||
id: uuidv4(),
|
||||
userId: workflowData.userId,
|
||||
name: 'Default API Key',
|
||||
key: newApiKeyVal,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
userKey = newApiKeyVal
|
||||
logger.info(`[${requestId}] Generated new API key for user: ${workflowData.userId}`)
|
||||
} catch (keyError) {
|
||||
// If key generation fails, log the error but continue with the request
|
||||
logger.error(`[${requestId}] Failed to generate API key:`, keyError)
|
||||
}
|
||||
} else {
|
||||
userKey = userApiKey[0].key
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the workflow has meaningful changes that would require redeployment
|
||||
@@ -139,10 +145,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
// Get the workflow to find the user (removed deprecated state column)
|
||||
// Get the workflow to find the user and existing pin (removed deprecated state column)
|
||||
const workflowData = await db
|
||||
.select({
|
||||
userId: workflow.userId,
|
||||
pinnedApiKey: workflow.pinnedApiKey,
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, id))
|
||||
@@ -155,6 +162,17 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
const userId = workflowData[0].userId
|
||||
|
||||
// Parse request body to capture selected API key (if provided)
|
||||
let providedApiKey: string | null = null
|
||||
try {
|
||||
const parsed = await request.json()
|
||||
if (parsed && typeof parsed.apiKey === 'string' && parsed.apiKey.trim().length > 0) {
|
||||
providedApiKey = parsed.apiKey.trim()
|
||||
}
|
||||
} catch (_err) {
|
||||
// Body may be empty; ignore
|
||||
}
|
||||
|
||||
// Get the current live state from normalized tables instead of stale JSON
|
||||
logger.debug(`[${requestId}] Getting current workflow state for deployment`)
|
||||
|
||||
@@ -241,13 +259,14 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const deployedAt = new Date()
|
||||
logger.debug(`[${requestId}] Proceeding with deployment at ${deployedAt.toISOString()}`)
|
||||
|
||||
// Check if the user already has an API key
|
||||
// Check if the user already has API keys
|
||||
const userApiKey = await db
|
||||
.select({
|
||||
key: apiKey.key,
|
||||
})
|
||||
.from(apiKey)
|
||||
.where(eq(apiKey.userId, userId))
|
||||
.orderBy(desc(apiKey.lastUsed), desc(apiKey.createdAt))
|
||||
.limit(1)
|
||||
|
||||
let userKey = null
|
||||
@@ -274,15 +293,42 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
userKey = userApiKey[0].key
|
||||
}
|
||||
|
||||
// If client provided a specific API key and it belongs to the user, prefer it
|
||||
if (providedApiKey) {
|
||||
const [owned] = await db
|
||||
.select({ key: apiKey.key })
|
||||
.from(apiKey)
|
||||
.where(and(eq(apiKey.userId, userId), eq(apiKey.key, providedApiKey)))
|
||||
.limit(1)
|
||||
if (owned) {
|
||||
userKey = providedApiKey
|
||||
}
|
||||
}
|
||||
|
||||
// Update the workflow deployment status and save current state as deployed state
|
||||
await db
|
||||
.update(workflow)
|
||||
.set({
|
||||
isDeployed: true,
|
||||
deployedAt,
|
||||
deployedState: currentState,
|
||||
})
|
||||
.where(eq(workflow.id, id))
|
||||
const updateData: any = {
|
||||
isDeployed: true,
|
||||
deployedAt,
|
||||
deployedState: currentState,
|
||||
}
|
||||
// Only pin when the client explicitly provided a key in this request
|
||||
if (providedApiKey) {
|
||||
updateData.pinnedApiKey = userKey
|
||||
}
|
||||
|
||||
await db.update(workflow).set(updateData).where(eq(workflow.id, id))
|
||||
|
||||
// Update lastUsed for the key we returned
|
||||
if (userKey) {
|
||||
try {
|
||||
await db
|
||||
.update(apiKey)
|
||||
.set({ lastUsed: new Date(), updatedAt: new Date() })
|
||||
.where(eq(apiKey.key, userKey))
|
||||
} catch (e) {
|
||||
logger.warn(`[${requestId}] Failed to update lastUsed for api key`)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Workflow deployed successfully: ${id}`)
|
||||
return createSuccessResponse({ apiKey: userKey, isDeployed: true, deployedAt })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||
@@ -56,22 +56,31 @@ export async function validateWorkflowAccess(
|
||||
}
|
||||
}
|
||||
|
||||
// Verify API key belongs to the user who owns the workflow
|
||||
const userApiKeys = await db
|
||||
.select({
|
||||
key: apiKey.key,
|
||||
})
|
||||
.from(apiKey)
|
||||
.where(eq(apiKey.userId, workflow.userId))
|
||||
// If a pinned key exists, only accept that specific key
|
||||
if (workflow.pinnedApiKey) {
|
||||
if (workflow.pinnedApiKey !== apiKeyHeader) {
|
||||
return {
|
||||
error: {
|
||||
message: 'Unauthorized: Invalid API key',
|
||||
status: 401,
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise, verify the key belongs to the workflow owner
|
||||
const [owned] = await db
|
||||
.select({ key: apiKey.key })
|
||||
.from(apiKey)
|
||||
.where(and(eq(apiKey.userId, workflow.userId), eq(apiKey.key, apiKeyHeader)))
|
||||
.limit(1)
|
||||
|
||||
const validApiKey = userApiKeys.some((k) => k.key === apiKeyHeader)
|
||||
|
||||
if (!validApiKey) {
|
||||
return {
|
||||
error: {
|
||||
message: 'Unauthorized: Invalid API key',
|
||||
status: 401,
|
||||
},
|
||||
if (!owned) {
|
||||
return {
|
||||
error: {
|
||||
message: 'Unauthorized: Invalid API key',
|
||||
status: 401,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ interface DeployFormProps {
|
||||
onSubmit: (data: DeployFormValues) => void
|
||||
getInputFormatExample: () => string
|
||||
onApiKeyCreated?: () => void
|
||||
// Optional id to bind an external submit button via the `form` attribute
|
||||
formId?: string
|
||||
}
|
||||
|
||||
export function DeployForm({
|
||||
@@ -69,6 +71,7 @@ export function DeployForm({
|
||||
onSubmit,
|
||||
getInputFormatExample,
|
||||
onApiKeyCreated,
|
||||
formId,
|
||||
}: DeployFormProps) {
|
||||
// State
|
||||
const [isCreatingKey, setIsCreatingKey] = useState(false)
|
||||
@@ -148,6 +151,7 @@ export function DeployForm({
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
id={formId}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onSubmit(form.getValues())
|
||||
|
||||
@@ -178,6 +178,7 @@ export function DeployModal({
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDeploymentInfo() {
|
||||
// If not open or not deployed, clear info and stop
|
||||
if (!open || !workflowId || !isDeployed) {
|
||||
setDeploymentInfo(null)
|
||||
if (!open) {
|
||||
@@ -186,6 +187,12 @@ export function DeployModal({
|
||||
return
|
||||
}
|
||||
|
||||
// If we already have deploymentInfo (e.g., just deployed and set locally), avoid overriding it
|
||||
if (deploymentInfo?.isDeployed && !needsRedeployment) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
@@ -215,7 +222,7 @@ export function DeployModal({
|
||||
}
|
||||
|
||||
fetchDeploymentInfo()
|
||||
}, [open, workflowId, isDeployed, needsRedeployment])
|
||||
}, [open, workflowId, isDeployed, needsRedeployment, deploymentInfo?.isDeployed])
|
||||
|
||||
const onDeploy = async (data: DeployFormValues) => {
|
||||
setApiDeployError(null)
|
||||
@@ -239,13 +246,13 @@ export function DeployModal({
|
||||
throw new Error(errorData.error || 'Failed to deploy workflow')
|
||||
}
|
||||
|
||||
const { isDeployed: newDeployStatus, deployedAt } = await response.json()
|
||||
const { isDeployed: newDeployStatus, deployedAt, apiKey } = await response.json()
|
||||
|
||||
setDeploymentStatus(
|
||||
workflowId,
|
||||
newDeployStatus,
|
||||
deployedAt ? new Date(deployedAt) : undefined,
|
||||
data.apiKey
|
||||
apiKey || data.apiKey
|
||||
)
|
||||
|
||||
setNeedsRedeployment(false)
|
||||
@@ -258,9 +265,9 @@ export function DeployModal({
|
||||
const newDeploymentInfo = {
|
||||
isDeployed: true,
|
||||
deployedAt: deployedAt,
|
||||
apiKey: data.apiKey,
|
||||
apiKey: apiKey || data.apiKey,
|
||||
endpoint,
|
||||
exampleCommand: `curl -X POST -H "X-API-Key: ${data.apiKey}" -H "Content-Type: application/json"${inputFormatExample} ${endpoint}`,
|
||||
exampleCommand: `curl -X POST -H "X-API-Key: ${apiKey || data.apiKey}" -H "Content-Type: application/json"${inputFormatExample} ${endpoint}`,
|
||||
needsRedeployment: false,
|
||||
}
|
||||
|
||||
@@ -331,6 +338,9 @@ export function DeployModal({
|
||||
}
|
||||
|
||||
await refetchDeployedState()
|
||||
|
||||
// Ensure modal status updates immediately
|
||||
setDeploymentInfo((prev) => (prev ? { ...prev, needsRedeployment: false } : prev))
|
||||
} catch (error: any) {
|
||||
logger.error('Error redeploying workflow:', { error })
|
||||
} finally {
|
||||
@@ -437,7 +447,9 @@ export function DeployModal({
|
||||
{isDeployed ? (
|
||||
<DeploymentInfo
|
||||
isLoading={isLoading}
|
||||
deploymentInfo={deploymentInfo}
|
||||
deploymentInfo={
|
||||
deploymentInfo ? { ...deploymentInfo, needsRedeployment } : null
|
||||
}
|
||||
onRedeploy={handleRedeploy}
|
||||
onUndeploy={handleUndeploy}
|
||||
isSubmitting={isSubmitting}
|
||||
@@ -464,6 +476,7 @@ export function DeployModal({
|
||||
onSubmit={onDeploy}
|
||||
getInputFormatExample={getInputFormatExample}
|
||||
onApiKeyCreated={fetchApiKeys}
|
||||
formId='deploy-api-form'
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -494,8 +507,8 @@ export function DeployModal({
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
onClick={() => onDeploy({ apiKey: apiKeys.length > 0 ? apiKeys[0].key : '' })}
|
||||
type='submit'
|
||||
form='deploy-api-form'
|
||||
disabled={isSubmitting || (!keysLoaded && !apiKeys.length)}
|
||||
className={cn(
|
||||
'gap-2 font-medium',
|
||||
|
||||
1
apps/sim/db/migrations/0071_free_sharon_carter.sql
Normal file
1
apps/sim/db/migrations/0071_free_sharon_carter.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "workflow" ADD COLUMN "pinned_api_key" text;
|
||||
5869
apps/sim/db/migrations/meta/0071_snapshot.json
Normal file
5869
apps/sim/db/migrations/meta/0071_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -491,6 +491,13 @@
|
||||
"when": 1754682155062,
|
||||
"tag": "0070_charming_wrecking_crew",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 71,
|
||||
"version": "7",
|
||||
"when": 1754719531015,
|
||||
"tag": "0071_free_sharon_carter",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -130,6 +130,8 @@ export const workflow = pgTable(
|
||||
isDeployed: boolean('is_deployed').notNull().default(false),
|
||||
deployedState: json('deployed_state'),
|
||||
deployedAt: timestamp('deployed_at'),
|
||||
// When set, only this API key is authorized for execution
|
||||
pinnedApiKey: text('pinned_api_key'),
|
||||
collaborators: json('collaborators').notNull().default('[]'),
|
||||
runCount: integer('run_count').notNull().default(0),
|
||||
lastRunAt: timestamp('last_run_at'),
|
||||
|
||||
Reference in New Issue
Block a user