Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions apps/sim/app/api/cron/renew-subscriptions/route.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use verifyCronAuth (verifyCronSecret)

Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { db } from '@sim/db'
import { webhook as webhookTable, workflow as workflowTable } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

const logger = createLogger('TeamsSubscriptionRenewal')

/**
* Cron endpoint to renew Microsoft Teams chat subscriptions before they expire
*
* Teams subscriptions expire after ~3 days and must be renewed.
* Configured in helm/sim/values.yaml under cronjobs.jobs.renewSubscriptions
*/
export async function GET(request: NextRequest) {
try {
const authError = verifyCronAuth(request, 'Teams subscription renewal')
if (authError) {
return authError
}

logger.info('Starting Teams subscription renewal job')

let totalRenewed = 0
let totalFailed = 0
let totalChecked = 0

// Get all active Microsoft Teams webhooks with their workflows
const webhooksWithWorkflows = await db
.select({
webhook: webhookTable,
workflow: workflowTable,
})
.from(webhookTable)
.innerJoin(workflowTable, eq(webhookTable.workflowId, workflowTable.id))
.where(and(eq(webhookTable.isActive, true), eq(webhookTable.provider, 'microsoftteams')))

logger.info(
`Found ${webhooksWithWorkflows.length} active Teams webhooks, checking for expiring subscriptions`
)

// Renewal threshold: 48 hours before expiration
const renewalThreshold = new Date(Date.now() + 48 * 60 * 60 * 1000)

for (const { webhook, workflow } of webhooksWithWorkflows) {
const config = (webhook.providerConfig as Record<string, any>) || {}

// Check if this is a Teams chat subscription that needs renewal
if (config.triggerId !== 'microsoftteams_chat_subscription') continue

const expirationStr = config.subscriptionExpiration as string | undefined
if (!expirationStr) continue

const expiresAt = new Date(expirationStr)
if (expiresAt > renewalThreshold) continue // Not expiring soon

totalChecked++

try {
logger.info(
`Renewing Teams subscription for webhook ${webhook.id} (expires: ${expiresAt.toISOString()})`
)

const credentialId = config.credentialId as string | undefined
const externalSubscriptionId = config.externalSubscriptionId as string | undefined

if (!credentialId || !externalSubscriptionId) {
logger.error(`Missing credentialId or externalSubscriptionId for webhook ${webhook.id}`)
totalFailed++
continue
}

// Get fresh access token
const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
workflow.userId,
`renewal-${webhook.id}`
)

if (!accessToken) {
logger.error(`Failed to get access token for webhook ${webhook.id}`)
totalFailed++
continue
}

// Extend subscription to maximum lifetime (4230 minutes = ~3 days)
const maxLifetimeMinutes = 4230
const newExpirationDateTime = new Date(
Date.now() + maxLifetimeMinutes * 60 * 1000
).toISOString()

const res = await fetch(
`https://graph.microsoft.com/v1.0/subscriptions/${externalSubscriptionId}`,
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ expirationDateTime: newExpirationDateTime }),
}
)

if (!res.ok) {
const error = await res.json()
logger.error(
`Failed to renew Teams subscription ${externalSubscriptionId} for webhook ${webhook.id}`,
{ status: res.status, error: error.error }
)
totalFailed++
continue
}

const payload = await res.json()

// Update webhook config with new expiration
const updatedConfig = {
...config,
subscriptionExpiration: payload.expirationDateTime,
}

await db
.update(webhookTable)
.set({ providerConfig: updatedConfig, updatedAt: new Date() })
.where(eq(webhookTable.id, webhook.id))

logger.info(
`Successfully renewed Teams subscription for webhook ${webhook.id}. New expiration: ${payload.expirationDateTime}`
)
totalRenewed++
} catch (error) {
logger.error(`Error renewing subscription for webhook ${webhook.id}:`, error)
totalFailed++
}
}

logger.info(
`Teams subscription renewal job completed. Checked: ${totalChecked}, Renewed: ${totalRenewed}, Failed: ${totalFailed}`
)

return NextResponse.json({
success: true,
checked: totalChecked,
renewed: totalRenewed,
failed: totalFailed,
total: webhooksWithWorkflows.length,
})
} catch (error) {
logger.error('Error in Teams subscription renewal job:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
25 changes: 13 additions & 12 deletions apps/sim/app/api/webhooks/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,10 +408,18 @@ export async function DELETE(
}
}

// If it's a Telegram webhook, delete it from Telegram first
// Delete Microsoft Teams subscription if applicable
if (foundWebhook.provider === 'microsoftteams') {
const { deleteTeamsSubscription } = await import('@/lib/webhooks/webhook-helpers')
logger.info(`[${requestId}] Deleting Teams subscription for webhook ${id}`)
await deleteTeamsSubscription(foundWebhook, webhookData.workflow, requestId)
// Don't fail webhook deletion if subscription cleanup fails
}

// Delete Telegram webhook if applicable
if (foundWebhook.provider === 'telegram') {
try {
const { botToken } = foundWebhook.providerConfig as { botToken: string }
const { botToken } = (foundWebhook.providerConfig || {}) as { botToken?: string }

if (!botToken) {
logger.warn(`[${requestId}] Missing botToken for Telegram webhook deletion.`, {
Expand All @@ -426,19 +434,15 @@ export async function DELETE(
const telegramApiUrl = `https://api.telegram.org/bot${botToken}/deleteWebhook`
const telegramResponse = await fetch(telegramApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: { 'Content-Type': 'application/json' },
})

const responseBody = await telegramResponse.json()
if (!telegramResponse.ok || !responseBody.ok) {
const errorMessage =
responseBody.description ||
`Failed to delete Telegram webhook. Status: ${telegramResponse.status}`
logger.error(`[${requestId}] ${errorMessage}`, {
response: responseBody,
})
logger.error(`[${requestId}] ${errorMessage}`, { response: responseBody })
return NextResponse.json(
{ error: 'Failed to delete webhook from Telegram', details: errorMessage },
{ status: 500 }
Expand All @@ -453,10 +457,7 @@ export async function DELETE(
stack: error.stack,
})
return NextResponse.json(
{
error: 'Failed to delete webhook from Telegram',
details: error.message,
},
{ error: 'Failed to delete webhook from Telegram', details: error.message },
{ status: 500 }
)
}
Expand Down
Loading