Files
sim/apps/sim/app/api/cron/renew-subscriptions/route.ts
Vikhyath Mondreti e55d41f2ef fix(credentials): credential dependent endpoints (#3309)
* fix(dependent): credential dependent endpoints

* fix tests

* fix route to not block ws creds"

* remove faulty auth checks:

* prevent unintended cascade by depends on during migration

* address bugbot comments
2026-02-23 04:38:03 -08:00

187 lines
5.9 KiB
TypeScript

import { db } from '@sim/db'
import { account, webhook as webhookTable } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
const logger = createLogger('TeamsSubscriptionRenewal')
async function getCredentialOwner(
credentialId: string
): Promise<{ userId: string; accountId: string } | null> {
const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
logger.error(`Failed to resolve OAuth account for credential ${credentialId}`)
return null
}
const [credentialRecord] = await db
.select({ userId: account.userId })
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)
return credentialRecord
? { userId: credentialRecord.userId, accountId: resolved.accountId }
: null
}
/**
* 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
const webhooksWithWorkflows = await db
.select({
webhook: webhookTable,
})
.from(webhookTable)
.where(
and(
eq(webhookTable.isActive, true),
or(
eq(webhookTable.provider, 'microsoft-teams'),
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 } 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
}
const credentialOwner = await getCredentialOwner(credentialId)
if (!credentialOwner) {
logger.error(`Credential owner not found for credential ${credentialId}`)
totalFailed++
continue
}
// Get fresh access token
const accessToken = await refreshAccessTokenIfNeeded(
credentialOwner.accountId,
credentialOwner.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 })
}
}