mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-05 20:25:08 -05:00
added custom oauth for tiktok
This commit is contained in:
@@ -6,9 +6,11 @@ import { getSession } from '@/lib/auth'
|
||||
import { refreshOAuthToken } from '@/lib/oauth'
|
||||
import {
|
||||
getMicrosoftRefreshTokenExpiry,
|
||||
getTikTokRefreshTokenExpiry,
|
||||
isMicrosoftProvider,
|
||||
isTikTokProvider,
|
||||
PROACTIVE_REFRESH_THRESHOLD_DAYS,
|
||||
} from '@/lib/oauth/microsoft'
|
||||
} from '@/lib/oauth/utils'
|
||||
|
||||
const logger = createLogger('OAuthUtilsAPI')
|
||||
|
||||
@@ -220,13 +222,13 @@ export async function refreshAccessTokenIfNeeded(
|
||||
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
|
||||
|
||||
// Check if we should proactively refresh to prevent refresh token expiry
|
||||
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
|
||||
// This applies to providers with expiring refresh tokens (Microsoft: 90 days, TikTok: 365 days)
|
||||
const proactiveRefreshThreshold = new Date(
|
||||
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
|
||||
)
|
||||
const refreshTokenNeedsProactiveRefresh =
|
||||
!!credential.refreshToken &&
|
||||
isMicrosoftProvider(credential.providerId) &&
|
||||
(isMicrosoftProvider(credential.providerId) || isTikTokProvider(credential.providerId)) &&
|
||||
refreshTokenExpiresAt &&
|
||||
refreshTokenExpiresAt <= proactiveRefreshThreshold
|
||||
|
||||
@@ -271,6 +273,8 @@ export async function refreshAccessTokenIfNeeded(
|
||||
|
||||
if (isMicrosoftProvider(credential.providerId)) {
|
||||
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
|
||||
} else if (isTikTokProvider(credential.providerId)) {
|
||||
updateData.refreshTokenExpiresAt = getTikTokRefreshTokenExpiry()
|
||||
}
|
||||
|
||||
// Update the token in the database
|
||||
@@ -321,13 +325,13 @@ export async function refreshTokenIfNeeded(
|
||||
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
|
||||
|
||||
// Check if we should proactively refresh to prevent refresh token expiry
|
||||
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
|
||||
// This applies to providers with expiring refresh tokens (Microsoft: 90 days, TikTok: 365 days)
|
||||
const proactiveRefreshThreshold = new Date(
|
||||
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
|
||||
)
|
||||
const refreshTokenNeedsProactiveRefresh =
|
||||
!!credential.refreshToken &&
|
||||
isMicrosoftProvider(credential.providerId) &&
|
||||
(isMicrosoftProvider(credential.providerId) || isTikTokProvider(credential.providerId)) &&
|
||||
refreshTokenExpiresAt &&
|
||||
refreshTokenExpiresAt <= proactiveRefreshThreshold
|
||||
|
||||
@@ -368,6 +372,8 @@ export async function refreshTokenIfNeeded(
|
||||
|
||||
if (isMicrosoftProvider(credential.providerId)) {
|
||||
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
|
||||
} else if (isTikTokProvider(credential.providerId)) {
|
||||
updateData.refreshTokenExpiresAt = getTikTokRefreshTokenExpiry()
|
||||
}
|
||||
|
||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||
|
||||
70
apps/sim/app/api/auth/tiktok/authorize/route.ts
Normal file
70
apps/sim/app/api/auth/tiktok/authorize/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
|
||||
const logger = createLogger('TikTokAuthorize')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const clientKey = env.TIKTOK_CLIENT_ID
|
||||
|
||||
if (!clientKey) {
|
||||
logger.error('TIKTOK_CLIENT_ID not configured')
|
||||
return NextResponse.json({ error: 'TikTok client key not configured' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Get the return URL from query params or use default
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const returnUrl = searchParams.get('returnUrl') || `${getBaseUrl()}/workspace`
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
const redirectUri = `${baseUrl}/api/auth/tiktok/callback`
|
||||
|
||||
// Generate a random state for CSRF protection
|
||||
const state = Buffer.from(
|
||||
JSON.stringify({
|
||||
returnUrl,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
).toString('base64url')
|
||||
|
||||
// TikTok scopes
|
||||
const scopes = [
|
||||
'user.info.basic',
|
||||
'user.info.profile',
|
||||
'user.info.stats',
|
||||
'video.list',
|
||||
'video.publish',
|
||||
]
|
||||
|
||||
// Build TikTok authorization URL with client_key (not client_id)
|
||||
// Note: TikTok expects raw commas in scope parameter, not URL-encoded %2C
|
||||
// So we manually construct the URL to avoid automatic encoding
|
||||
const scopeString = scopes.join(',')
|
||||
const encodedRedirectUri = encodeURIComponent(redirectUri)
|
||||
const encodedState = encodeURIComponent(state)
|
||||
|
||||
const authUrl = `https://www.tiktok.com/v2/auth/authorize/?client_key=${clientKey}&response_type=code&scope=${scopeString}&redirect_uri=${encodedRedirectUri}&state=${encodedState}`
|
||||
|
||||
logger.info('Redirecting to TikTok authorization', {
|
||||
clientKey: clientKey ? `${clientKey.substring(0, 8)}...` : 'NOT SET',
|
||||
redirectUri,
|
||||
scopes: scopeString,
|
||||
fullUrl: authUrl,
|
||||
})
|
||||
|
||||
return NextResponse.redirect(authUrl)
|
||||
} catch (error) {
|
||||
logger.error('Error initiating TikTok authorization:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
130
apps/sim/app/api/auth/tiktok/callback/route.ts
Normal file
130
apps/sim/app/api/auth/tiktok/callback/route.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
|
||||
const logger = createLogger('TikTokCallback')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.error('No session found during TikTok callback')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`)
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const code = searchParams.get('code')
|
||||
const state = searchParams.get('state')
|
||||
const error = searchParams.get('error')
|
||||
const errorDescription = searchParams.get('error_description')
|
||||
|
||||
// Handle errors from TikTok
|
||||
if (error) {
|
||||
logger.error('TikTok authorization error:', { error, errorDescription })
|
||||
return NextResponse.redirect(
|
||||
`${baseUrl}/workspace?error=tiktok_auth_failed&message=${encodeURIComponent(errorDescription || error)}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
logger.error('No authorization code received from TikTok')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=no_code`)
|
||||
}
|
||||
|
||||
// Parse state to get return URL
|
||||
let returnUrl = `${baseUrl}/workspace`
|
||||
if (state) {
|
||||
try {
|
||||
const stateData = JSON.parse(Buffer.from(state, 'base64url').toString())
|
||||
returnUrl = stateData.returnUrl || returnUrl
|
||||
} catch {
|
||||
logger.warn('Failed to parse state parameter')
|
||||
}
|
||||
}
|
||||
|
||||
const clientKey = env.TIKTOK_CLIENT_ID
|
||||
const clientSecret = env.TIKTOK_CLIENT_SECRET
|
||||
|
||||
if (!clientKey || !clientSecret) {
|
||||
logger.error('TikTok credentials not configured')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=config_error`)
|
||||
}
|
||||
|
||||
const redirectUri = `${baseUrl}/api/auth/tiktok/callback`
|
||||
|
||||
// Exchange authorization code for access token
|
||||
// TikTok uses client_key instead of client_id
|
||||
const tokenResponse = await fetch('https://open.tiktokapis.com/v2/oauth/token/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_key: clientKey,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri,
|
||||
}).toString(),
|
||||
})
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text()
|
||||
logger.error('Failed to exchange code for token:', {
|
||||
status: tokenResponse.status,
|
||||
error: errorText,
|
||||
})
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=token_exchange_failed`)
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json()
|
||||
|
||||
if (tokenData.error) {
|
||||
logger.error('TikTok token error:', tokenData)
|
||||
return NextResponse.redirect(
|
||||
`${baseUrl}/workspace?error=tiktok_token_error&message=${encodeURIComponent(tokenData.error_description || tokenData.error)}`
|
||||
)
|
||||
}
|
||||
|
||||
const { access_token, refresh_token, expires_in, open_id, scope } = tokenData
|
||||
|
||||
if (!access_token) {
|
||||
logger.error('No access token in TikTok response:', tokenData)
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=no_access_token`)
|
||||
}
|
||||
|
||||
// Store the tokens by calling the store endpoint
|
||||
const storeResponse = await fetch(`${baseUrl}/api/auth/tiktok/store`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: request.headers.get('cookie') || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token,
|
||||
expiresIn: expires_in,
|
||||
openId: open_id,
|
||||
scope,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!storeResponse.ok) {
|
||||
const storeError = await storeResponse.text()
|
||||
logger.error('Failed to store TikTok tokens:', storeError)
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=store_failed`)
|
||||
}
|
||||
|
||||
logger.info('TikTok authorization successful')
|
||||
return NextResponse.redirect(`${returnUrl}?tiktok_connected=true`)
|
||||
} catch (error) {
|
||||
logger.error('Error in TikTok callback:', error)
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=callback_error`)
|
||||
}
|
||||
}
|
||||
108
apps/sim/app/api/auth/tiktok/store/route.ts
Normal file
108
apps/sim/app/api/auth/tiktok/store/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getTikTokRefreshTokenExpiry } from '@/lib/oauth/utils'
|
||||
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
|
||||
import { db } from '@/../../packages/db'
|
||||
import { account } from '@/../../packages/db/schema'
|
||||
|
||||
const logger = createLogger('TikTokStore')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn('Unauthorized attempt to store TikTok token')
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { accessToken, refreshToken, expiresIn, openId, scope } = body
|
||||
|
||||
if (!accessToken || !openId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Access token and open_id required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch user info from TikTok to get display name
|
||||
let displayName = 'TikTok User'
|
||||
let avatarUrl: string | undefined
|
||||
|
||||
try {
|
||||
const userResponse = await fetch(
|
||||
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,union_id,avatar_url,display_name',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (userResponse.ok) {
|
||||
const userData = await userResponse.json()
|
||||
if (userData.data?.user) {
|
||||
displayName = userData.data.user.display_name || displayName
|
||||
avatarUrl = userData.data.user.avatar_url
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to fetch TikTok user info:', error)
|
||||
}
|
||||
|
||||
const existing = await db.query.account.findFirst({
|
||||
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'tiktok')),
|
||||
})
|
||||
|
||||
const now = new Date()
|
||||
const accessTokenExpiresAt = expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined
|
||||
const refreshTokenExpiresAt = getTikTokRefreshTokenExpiry()
|
||||
|
||||
if (existing) {
|
||||
await db
|
||||
.update(account)
|
||||
.set({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accountId: openId,
|
||||
scope:
|
||||
scope || 'user.info.basic,user.info.profile,user.info.stats,video.list,video.publish',
|
||||
accessTokenExpiresAt,
|
||||
refreshTokenExpiresAt,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(account.id, existing.id))
|
||||
|
||||
logger.info('Updated existing TikTok account', { accountId: openId })
|
||||
} else {
|
||||
await safeAccountInsert(
|
||||
{
|
||||
id: `tiktok_${session.user.id}_${Date.now()}`,
|
||||
userId: session.user.id,
|
||||
providerId: 'tiktok',
|
||||
accountId: openId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
scope:
|
||||
scope || 'user.info.basic,user.info.profile,user.info.stats,video.list,video.publish',
|
||||
accessTokenExpiresAt,
|
||||
refreshTokenExpiresAt,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
{ provider: 'TikTok', identifier: openId }
|
||||
)
|
||||
|
||||
logger.info('Created new TikTok account', { accountId: openId })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Error storing TikTok token:', error)
|
||||
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -294,6 +294,13 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'user-follow-modify': 'Follow and unfollow artists and users',
|
||||
'user-read-playback-position': 'View playback position in podcasts',
|
||||
'ugc-image-upload': 'Upload images to Spotify playlists',
|
||||
// TikTok scopes
|
||||
'user.info.basic': 'View basic profile info (avatar, display name)',
|
||||
'user.info.profile': 'View profile details (bio, verified status)',
|
||||
'user.info.stats': 'View account statistics (likes, followers, video count)',
|
||||
'video.list': 'View public videos',
|
||||
'video.publish': 'Post content to profile',
|
||||
'video.upload': 'Upload content as draft',
|
||||
}
|
||||
|
||||
function getScopeDescription(scope: string): string {
|
||||
@@ -373,6 +380,13 @@ export function OAuthRequiredModal({
|
||||
return
|
||||
}
|
||||
|
||||
if (providerId === 'tiktok') {
|
||||
onClose()
|
||||
const returnUrl = encodeURIComponent(window.location.href)
|
||||
window.location.href = `/api/auth/tiktok/authorize?returnUrl=${returnUrl}`
|
||||
return
|
||||
}
|
||||
|
||||
await client.oauth2.link({
|
||||
providerId,
|
||||
callbackURL: window.location.href,
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
renderPasswordResetEmail,
|
||||
renderWelcomeEmail,
|
||||
} from '@/components/emails'
|
||||
import { createAnonymousSession, ensureAnonymousUserExists } from '@/lib/auth/anonymous'
|
||||
import { SSO_TRUSTED_PROVIDERS } from '@/lib/auth/sso/constants'
|
||||
import { sendPlanWelcomeEmail } from '@/lib/billing'
|
||||
import { authorizeSubscriptionReference } from '@/lib/billing/authorization'
|
||||
import { handleNewUser } from '@/lib/billing/core/usage'
|
||||
@@ -59,12 +61,15 @@ import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||
import { createAnonymousSession, ensureAnonymousUserExists } from './anonymous'
|
||||
import { SSO_TRUSTED_PROVIDERS } from './sso/constants'
|
||||
|
||||
const logger = createLogger('Auth')
|
||||
|
||||
import { getMicrosoftRefreshTokenExpiry, isMicrosoftProvider } from '@/lib/oauth/microsoft'
|
||||
import {
|
||||
getMicrosoftRefreshTokenExpiry,
|
||||
getTikTokRefreshTokenExpiry,
|
||||
isMicrosoftProvider,
|
||||
isTikTokProvider,
|
||||
} from '@/lib/oauth/utils'
|
||||
|
||||
const validStripeKey = env.STRIPE_SECRET_KEY
|
||||
|
||||
@@ -191,7 +196,9 @@ export const auth = betterAuth({
|
||||
|
||||
const refreshTokenExpiresAt = isMicrosoftProvider(account.providerId)
|
||||
? getMicrosoftRefreshTokenExpiry()
|
||||
: account.refreshTokenExpiresAt
|
||||
: isTikTokProvider(account.providerId)
|
||||
? getTikTokRefreshTokenExpiry()
|
||||
: account.refreshTokenExpiresAt
|
||||
|
||||
await db
|
||||
.update(schema.account)
|
||||
@@ -316,6 +323,13 @@ export const auth = betterAuth({
|
||||
.where(eq(schema.account.id, account.id))
|
||||
}
|
||||
|
||||
if (isTikTokProvider(account.providerId)) {
|
||||
await db
|
||||
.update(schema.account)
|
||||
.set({ refreshTokenExpiresAt: getTikTokRefreshTokenExpiry() })
|
||||
.where(eq(schema.account.id, account.id))
|
||||
}
|
||||
|
||||
// Sync webhooks for credential sets after connecting a new credential
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const userMemberships = await db
|
||||
@@ -2495,66 +2509,10 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
|
||||
// TikTok provider
|
||||
{
|
||||
providerId: 'tiktok',
|
||||
clientId: env.TIKTOK_CLIENT_ID as string,
|
||||
clientSecret: env.TIKTOK_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://www.tiktok.com/v2/auth/authorize/',
|
||||
tokenUrl: 'https://open.tiktokapis.com/v2/oauth/token/',
|
||||
scopes: [
|
||||
'user.info.basic',
|
||||
'user.info.profile',
|
||||
'user.info.stats',
|
||||
'video.list',
|
||||
'video.publish',
|
||||
],
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/tiktok`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
logger.info('Fetching TikTok user profile')
|
||||
|
||||
const response = await fetch(
|
||||
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,union_id,avatar_url,display_name',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('Failed to fetch TikTok user info', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
throw new Error('Failed to fetch user info')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const profile = data.data?.user
|
||||
|
||||
if (!profile) {
|
||||
logger.error('No user data in TikTok response')
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${profile.open_id}-${crypto.randomUUID()}`,
|
||||
name: profile.display_name || 'TikTok User',
|
||||
email: `${profile.open_id}@tiktok.user`,
|
||||
emailVerified: false,
|
||||
image: profile.avatar_url || undefined,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in TikTok getUserInfo:', { error })
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
// TikTok provider - REMOVED from generic OAuth
|
||||
// TikTok uses non-standard OAuth (client_key instead of client_id)
|
||||
// and cannot work with the generic OAuth plugin.
|
||||
// TikTok OAuth is handled via custom routes at /api/auth/tiktok/*
|
||||
|
||||
// WordPress.com provider
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './microsoft'
|
||||
export * from './oauth'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
export const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90
|
||||
export const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7
|
||||
|
||||
export const MICROSOFT_PROVIDERS = new Set([
|
||||
'microsoft-excel',
|
||||
'microsoft-planner',
|
||||
'microsoft-teams',
|
||||
'outlook',
|
||||
'onedrive',
|
||||
'sharepoint',
|
||||
])
|
||||
|
||||
export function isMicrosoftProvider(providerId: string): boolean {
|
||||
return MICROSOFT_PROVIDERS.has(providerId)
|
||||
}
|
||||
|
||||
export function getMicrosoftRefreshTokenExpiry(): Date {
|
||||
return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
@@ -832,6 +832,11 @@ interface ProviderAuthConfig {
|
||||
* instead of in the request body. Used by Cal.com.
|
||||
*/
|
||||
refreshTokenInAuthHeader?: boolean
|
||||
/**
|
||||
* Custom parameter name for client ID in request body.
|
||||
* Defaults to 'client_id'. TikTok uses 'client_key'.
|
||||
*/
|
||||
clientIdParamName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1168,6 +1173,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
|
||||
clientSecret,
|
||||
useBasicAuth: false,
|
||||
supportsRefreshTokenRotation: true,
|
||||
clientIdParamName: 'client_key', // TikTok uses client_key instead of client_id
|
||||
}
|
||||
}
|
||||
default:
|
||||
@@ -1206,7 +1212,9 @@ function buildAuthRequest(
|
||||
headers.Authorization = `Basic ${basicAuth}`
|
||||
} else {
|
||||
// Use body credentials - include client credentials in request body
|
||||
bodyParams.client_id = config.clientId
|
||||
// Use custom param name if specified (e.g., TikTok uses 'client_key' instead of 'client_id')
|
||||
const clientIdParam = config.clientIdParamName || 'client_id'
|
||||
bodyParams[clientIdParam] = config.clientId
|
||||
if (config.clientSecret) {
|
||||
bodyParams.client_secret = config.clientSecret
|
||||
}
|
||||
|
||||
@@ -7,6 +7,49 @@ import type {
|
||||
ScopeEvaluation,
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// Refresh Token Configuration
|
||||
// =============================================================================
|
||||
|
||||
// Microsoft refresh token configuration (90 days)
|
||||
const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90
|
||||
export const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7
|
||||
|
||||
const MICROSOFT_PROVIDERS = new Set([
|
||||
'microsoft-excel',
|
||||
'microsoft-planner',
|
||||
'microsoft-teams',
|
||||
'outlook',
|
||||
'onedrive',
|
||||
'sharepoint',
|
||||
])
|
||||
|
||||
export function isMicrosoftProvider(providerId: string): boolean {
|
||||
return MICROSOFT_PROVIDERS.has(providerId)
|
||||
}
|
||||
|
||||
export function getMicrosoftRefreshTokenExpiry(): Date {
|
||||
return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
// TikTok refresh token configuration (365 days)
|
||||
// TikTok access tokens expire in 24 hours, refresh tokens are valid for 365 days
|
||||
const TIKTOK_REFRESH_TOKEN_LIFETIME_DAYS = 365
|
||||
|
||||
const TIKTOK_PROVIDERS = new Set(['tiktok'])
|
||||
|
||||
export function isTikTokProvider(providerId: string): boolean {
|
||||
return TIKTOK_PROVIDERS.has(providerId)
|
||||
}
|
||||
|
||||
export function getTikTokRefreshTokenExpiry(): Date {
|
||||
return new Date(Date.now() + TIKTOK_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// OAuth Service Utilities
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Returns a flat list of all available OAuth services with metadata.
|
||||
* This is safe to use on the server as it doesn't include React components.
|
||||
|
||||
Reference in New Issue
Block a user