@nextsparkjs/plugin-social-media-publisher 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +76 -0
- package/README.md +423 -0
- package/api/social/connect/callback/route.ts +669 -0
- package/api/social/connect/route.ts +327 -0
- package/api/social/disconnect/route.ts +187 -0
- package/api/social/publish/route.ts +402 -0
- package/docs/01-getting-started/01-introduction.md +471 -0
- package/docs/01-getting-started/02-installation.md +471 -0
- package/docs/01-getting-started/03-configuration.md +515 -0
- package/docs/02-core-features/01-oauth-integration.md +501 -0
- package/docs/02-core-features/02-publishing.md +527 -0
- package/docs/02-core-features/03-token-management.md +661 -0
- package/docs/02-core-features/04-audit-logging.md +646 -0
- package/docs/03-advanced-usage/01-provider-apis.md +764 -0
- package/docs/03-advanced-usage/02-custom-integrations.md +695 -0
- package/docs/03-advanced-usage/03-per-client-architecture.md +575 -0
- package/docs/04-use-cases/01-agency-management.md +661 -0
- package/docs/04-use-cases/02-content-publishing.md +668 -0
- package/docs/04-use-cases/03-analytics-reporting.md +748 -0
- package/entities/audit-logs/audit-logs.config.ts +150 -0
- package/lib/oauth-helper.ts +167 -0
- package/lib/providers/facebook.ts +672 -0
- package/lib/providers/index.ts +21 -0
- package/lib/providers/instagram.ts +791 -0
- package/lib/validation.ts +155 -0
- package/migrations/001_social_media_tables.sql +167 -0
- package/package.json +15 -0
- package/plugin.config.ts +81 -0
- package/tsconfig.json +47 -0
- package/types/social.types.ts +171 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Media Connect Endpoint
|
|
3
|
+
*
|
|
4
|
+
* OAuth initiator and callback handler for connecting Facebook Pages and Instagram Business Accounts
|
|
5
|
+
* Accessible via: /api/v1/plugin/social-media-publisher/social/connect
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
9
|
+
import { authenticateRequest } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
10
|
+
import { TokenEncryption } from '@nextsparkjs/core/lib/oauth/encryption'
|
|
11
|
+
import { FacebookAPI } from '../../../lib/providers/facebook'
|
|
12
|
+
import { ConnectAccountSchema } from '../../../lib/validation'
|
|
13
|
+
import { exchangeCodeForToken, getOAuthConfig, generateAuthorizationUrl } from '../../../lib/oauth-helper'
|
|
14
|
+
import type { SocialPlatform } from '../../../types/social.types'
|
|
15
|
+
import { mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GET - Initiate OAuth flow
|
|
19
|
+
* Redirects user to Facebook OAuth authorization page
|
|
20
|
+
*/
|
|
21
|
+
export async function GET(request: NextRequest) {
|
|
22
|
+
try {
|
|
23
|
+
const { searchParams } = new URL(request.url)
|
|
24
|
+
const platform = searchParams.get('platform') || 'instagram_business'
|
|
25
|
+
const clientId = searchParams.get('clientId')
|
|
26
|
+
const randomState = searchParams.get('state') || ''
|
|
27
|
+
const mode = searchParams.get('mode') // 'preview' or undefined (save mode)
|
|
28
|
+
|
|
29
|
+
// Validate required parameters
|
|
30
|
+
if (!clientId) {
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{
|
|
33
|
+
error: 'Missing clientId',
|
|
34
|
+
details: 'clientId parameter is required to associate social accounts with the correct client',
|
|
35
|
+
},
|
|
36
|
+
{ status: 400 }
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate platform
|
|
41
|
+
if (platform !== 'facebook_page' && platform !== 'instagram_business') {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{
|
|
44
|
+
error: 'Invalid platform',
|
|
45
|
+
details: 'Platform must be "facebook_page" or "instagram_business"',
|
|
46
|
+
},
|
|
47
|
+
{ status: 400 }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Build state with clientId, platform, and mode embedded
|
|
52
|
+
// Format: "{randomState}&platform={platform}&clientId={clientId}&mode={mode}"
|
|
53
|
+
let state = `${randomState}&platform=${platform}&clientId=${clientId}`
|
|
54
|
+
if (mode) {
|
|
55
|
+
state += `&mode=${mode}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get OAuth configuration
|
|
59
|
+
const oauthConfig = getOAuthConfig()
|
|
60
|
+
|
|
61
|
+
console.log('[social-connect] OAuth Config:', {
|
|
62
|
+
facebookClientId: oauthConfig.facebookClientId,
|
|
63
|
+
redirectUri: oauthConfig.redirectUri,
|
|
64
|
+
baseUrl: process.env.NEXT_PUBLIC_APP_URL,
|
|
65
|
+
platform
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Generate authorization URL with proper scopes for platform
|
|
69
|
+
const authorizationUrl = generateAuthorizationUrl(
|
|
70
|
+
platform as 'facebook_page' | 'instagram_business',
|
|
71
|
+
oauthConfig,
|
|
72
|
+
state
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
console.log('[social-connect] Generated Authorization URL:', authorizationUrl)
|
|
76
|
+
console.log('[social-connect] Redirecting to Instagram OAuth:', {
|
|
77
|
+
platform,
|
|
78
|
+
clientId,
|
|
79
|
+
mode,
|
|
80
|
+
state,
|
|
81
|
+
redirectUri: oauthConfig.redirectUri,
|
|
82
|
+
authUrl: authorizationUrl
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Redirect to Facebook OAuth
|
|
86
|
+
return NextResponse.redirect(authorizationUrl)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('❌ OAuth initiation error:', error)
|
|
89
|
+
return NextResponse.json(
|
|
90
|
+
{
|
|
91
|
+
error: 'Failed to initiate OAuth',
|
|
92
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
93
|
+
},
|
|
94
|
+
{ status: 500 }
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* POST - Handle OAuth callback (deprecated - use /callback endpoint instead)
|
|
101
|
+
* This endpoint receives the authorization code and exchanges it for access token
|
|
102
|
+
*/
|
|
103
|
+
export async function POST(request: NextRequest) {
|
|
104
|
+
try {
|
|
105
|
+
// 1. Authentication
|
|
106
|
+
const authResult = await authenticateRequest(request)
|
|
107
|
+
if (!authResult.success) {
|
|
108
|
+
return NextResponse.json(
|
|
109
|
+
{ error: 'Authentication required' },
|
|
110
|
+
{ status: 401 }
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 2. Parse and validate request body
|
|
115
|
+
const body = await request.json()
|
|
116
|
+
const validation = ConnectAccountSchema.safeParse(body)
|
|
117
|
+
|
|
118
|
+
if (!validation.success) {
|
|
119
|
+
return NextResponse.json(
|
|
120
|
+
{
|
|
121
|
+
error: 'Validation failed',
|
|
122
|
+
details: validation.error.issues,
|
|
123
|
+
},
|
|
124
|
+
{ status: 400 }
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { code, state, platform } = validation.data
|
|
129
|
+
|
|
130
|
+
// 3. Verify state to prevent CSRF
|
|
131
|
+
// TODO: Implement state validation with session storage
|
|
132
|
+
// For now, we'll just log it
|
|
133
|
+
console.log('[social-connect] OAuth state:', state)
|
|
134
|
+
|
|
135
|
+
// 4. Exchange authorization code for access token
|
|
136
|
+
const oauthConfig = getOAuthConfig()
|
|
137
|
+
const tokenData = await exchangeCodeForToken(
|
|
138
|
+
code,
|
|
139
|
+
oauthConfig,
|
|
140
|
+
platform as 'facebook_page' | 'instagram_business'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const userAccessToken = tokenData.accessToken
|
|
144
|
+
const expiresIn = tokenData.expiresIn
|
|
145
|
+
|
|
146
|
+
// 5. Get accounts based on platform
|
|
147
|
+
let accountsToConnect: Array<{
|
|
148
|
+
platformAccountId: string
|
|
149
|
+
username: string
|
|
150
|
+
accessToken: string
|
|
151
|
+
permissions: string[]
|
|
152
|
+
metadata: any
|
|
153
|
+
}> = []
|
|
154
|
+
|
|
155
|
+
if (platform === 'facebook_page') {
|
|
156
|
+
// Get Facebook Pages
|
|
157
|
+
const pages = await FacebookAPI.getUserPages(userAccessToken)
|
|
158
|
+
|
|
159
|
+
// For each page, get detailed stats
|
|
160
|
+
for (const page of pages) {
|
|
161
|
+
try {
|
|
162
|
+
const pageInfo = await FacebookAPI.getPageInfo(page.id, page.accessToken)
|
|
163
|
+
|
|
164
|
+
accountsToConnect.push({
|
|
165
|
+
platformAccountId: page.id,
|
|
166
|
+
username: page.name,
|
|
167
|
+
accessToken: page.accessToken, // Use page token, not user token
|
|
168
|
+
permissions: page.tasks || [],
|
|
169
|
+
metadata: {
|
|
170
|
+
profilePictureUrl: pageInfo.profilePictureUrl,
|
|
171
|
+
fanCount: pageInfo.fanCount,
|
|
172
|
+
about: pageInfo.about,
|
|
173
|
+
category: pageInfo.category || page.category,
|
|
174
|
+
link: pageInfo.link,
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(`[social-connect] Failed to get stats for page ${page.id}:`, error)
|
|
179
|
+
// Fallback: add page without detailed stats
|
|
180
|
+
accountsToConnect.push({
|
|
181
|
+
platformAccountId: page.id,
|
|
182
|
+
username: page.name,
|
|
183
|
+
accessToken: page.accessToken,
|
|
184
|
+
permissions: page.tasks || [],
|
|
185
|
+
metadata: {
|
|
186
|
+
category: page.category,
|
|
187
|
+
pictureUrl: page.pictureUrl,
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else if (platform === 'instagram_business') {
|
|
193
|
+
// Get Facebook Pages first (Instagram Business Accounts are linked to Pages)
|
|
194
|
+
const pages = await FacebookAPI.getUserPages(userAccessToken)
|
|
195
|
+
|
|
196
|
+
// For each page, check if it has an Instagram Business Account
|
|
197
|
+
for (const page of pages) {
|
|
198
|
+
const igAccount = await FacebookAPI.getInstagramBusinessAccount(
|
|
199
|
+
page.id,
|
|
200
|
+
page.accessToken
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if (igAccount) {
|
|
204
|
+
accountsToConnect.push({
|
|
205
|
+
platformAccountId: igAccount.id,
|
|
206
|
+
username: igAccount.username,
|
|
207
|
+
accessToken: page.accessToken, // Use page token (has IG permissions)
|
|
208
|
+
permissions: ['instagram_basic', 'instagram_content_publish', 'instagram_manage_comments'],
|
|
209
|
+
metadata: {
|
|
210
|
+
profilePictureUrl: igAccount.profilePictureUrl,
|
|
211
|
+
followersCount: igAccount.followersCount,
|
|
212
|
+
followsCount: igAccount.followsCount,
|
|
213
|
+
mediaCount: igAccount.mediaCount,
|
|
214
|
+
linkedPageId: page.id,
|
|
215
|
+
linkedPageName: page.name,
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (accountsToConnect.length === 0) {
|
|
222
|
+
return NextResponse.json(
|
|
223
|
+
{
|
|
224
|
+
error: 'No Instagram Business Accounts found',
|
|
225
|
+
message:
|
|
226
|
+
'No Instagram Business Accounts are linked to your Facebook Pages. ' +
|
|
227
|
+
'Please connect an Instagram Business Account to one of your Facebook Pages first.',
|
|
228
|
+
},
|
|
229
|
+
{ status: 404 }
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 6. Encrypt tokens and save accounts
|
|
235
|
+
const savedAccounts = []
|
|
236
|
+
|
|
237
|
+
for (const account of accountsToConnect) {
|
|
238
|
+
// Encrypt access token
|
|
239
|
+
const encryptedToken = await TokenEncryption.encrypt(account.accessToken)
|
|
240
|
+
|
|
241
|
+
// Calculate expiration date
|
|
242
|
+
const now = new Date()
|
|
243
|
+
const expiresAt = new Date(now.getTime() + expiresIn * 1000)
|
|
244
|
+
|
|
245
|
+
// Insert social account into database
|
|
246
|
+
const result = await mutateWithRLS<{ id: string; platform: string; username: string; createdAt: string; permissions: string; accountMetadata: string }>(
|
|
247
|
+
`INSERT INTO "social_accounts"
|
|
248
|
+
("userId", platform, "platformAccountId", "username", "accessToken", "tokenExpiresAt", permissions, "accountMetadata", "isActive")
|
|
249
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
250
|
+
ON CONFLICT ("platformAccountId")
|
|
251
|
+
DO UPDATE SET
|
|
252
|
+
"accessToken" = EXCLUDED."accessToken",
|
|
253
|
+
"tokenExpiresAt" = EXCLUDED."tokenExpiresAt",
|
|
254
|
+
permissions = EXCLUDED.permissions,
|
|
255
|
+
"accountMetadata" = EXCLUDED."accountMetadata",
|
|
256
|
+
"isActive" = EXCLUDED."isActive",
|
|
257
|
+
"updatedAt" = CURRENT_TIMESTAMP
|
|
258
|
+
RETURNING *`,
|
|
259
|
+
[
|
|
260
|
+
authResult.user!.id,
|
|
261
|
+
platform,
|
|
262
|
+
account.platformAccountId,
|
|
263
|
+
account.username,
|
|
264
|
+
`${encryptedToken.encrypted}:${encryptedToken.iv}:${encryptedToken.keyId}`,
|
|
265
|
+
expiresAt.toISOString(),
|
|
266
|
+
JSON.stringify(account.permissions),
|
|
267
|
+
JSON.stringify(account.metadata),
|
|
268
|
+
true
|
|
269
|
+
],
|
|
270
|
+
authResult.user!.id
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
const savedAccount = result.rows[0]!
|
|
274
|
+
savedAccounts.push(savedAccount)
|
|
275
|
+
|
|
276
|
+
// Create audit log entry
|
|
277
|
+
await mutateWithRLS(
|
|
278
|
+
`INSERT INTO "audit_logs"
|
|
279
|
+
("userId", "accountId", action, details, "ipAddress", "userAgent")
|
|
280
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
281
|
+
[
|
|
282
|
+
authResult.user!.id,
|
|
283
|
+
savedAccount.id,
|
|
284
|
+
'account_connected',
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
platform,
|
|
287
|
+
accountName: account.username,
|
|
288
|
+
success: true,
|
|
289
|
+
connectedAt: new Date().toISOString()
|
|
290
|
+
}),
|
|
291
|
+
request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null,
|
|
292
|
+
request.headers.get('user-agent') || null
|
|
293
|
+
],
|
|
294
|
+
authResult.user!.id
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
console.log('[social-connect] ✅ Account saved and audit log created:', {
|
|
298
|
+
accountId: savedAccount.id,
|
|
299
|
+
platform,
|
|
300
|
+
accountName: account.username,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return NextResponse.json({
|
|
305
|
+
success: true,
|
|
306
|
+
message: `Successfully connected ${savedAccounts.length} ${platform} account(s)`,
|
|
307
|
+
accounts: savedAccounts.map((acc: any) => ({
|
|
308
|
+
id: acc.id,
|
|
309
|
+
platform: acc.platform,
|
|
310
|
+
accountName: acc.username,
|
|
311
|
+
permissions: typeof acc.permissions === 'string' ? JSON.parse(acc.permissions) : acc.permissions,
|
|
312
|
+
metadata: typeof acc.accountMetadata === 'string' ? JSON.parse(acc.accountMetadata) : acc.accountMetadata,
|
|
313
|
+
connectedAt: acc.createdAt,
|
|
314
|
+
})),
|
|
315
|
+
})
|
|
316
|
+
} catch (error: unknown) {
|
|
317
|
+
console.error('❌ Social connect error:', error)
|
|
318
|
+
|
|
319
|
+
return NextResponse.json(
|
|
320
|
+
{
|
|
321
|
+
error: 'Failed to connect social account',
|
|
322
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
323
|
+
},
|
|
324
|
+
{ status: 500 }
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Media Disconnect Endpoint
|
|
3
|
+
*
|
|
4
|
+
* Disconnects a social media account (marks as inactive)
|
|
5
|
+
* Accessible via: /api/v1/plugin/social-media-publisher/social/disconnect
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
9
|
+
import { authenticateRequest } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
10
|
+
import { DisconnectAccountSchema } from '../../../lib/validation'
|
|
11
|
+
import { queryOneWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
12
|
+
|
|
13
|
+
export async function POST(request: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
// 1. Authentication
|
|
16
|
+
const authResult = await authenticateRequest(request)
|
|
17
|
+
if (!authResult.success) {
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: 'Authentication required' },
|
|
20
|
+
{ status: 401 }
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Parse and validate request body
|
|
25
|
+
const body = await request.json()
|
|
26
|
+
const validation = DisconnectAccountSchema.safeParse(body)
|
|
27
|
+
|
|
28
|
+
if (!validation.success) {
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{
|
|
31
|
+
error: 'Validation failed',
|
|
32
|
+
details: validation.error.issues,
|
|
33
|
+
},
|
|
34
|
+
{ status: 400 }
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { accountId } = validation.data
|
|
39
|
+
|
|
40
|
+
// 3. Get social account from database
|
|
41
|
+
const account = await queryOneWithRLS<{
|
|
42
|
+
id: string
|
|
43
|
+
userId: string
|
|
44
|
+
platform: string
|
|
45
|
+
platformAccountId: string
|
|
46
|
+
username: string
|
|
47
|
+
isActive: boolean
|
|
48
|
+
}>(
|
|
49
|
+
`SELECT id, "userId", platform, "platformAccountId", "username", "isActive"
|
|
50
|
+
FROM "social_accounts"
|
|
51
|
+
WHERE id = $1 AND "userId" = $2`,
|
|
52
|
+
[accountId, authResult.user!.id],
|
|
53
|
+
authResult.user!.id
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Verify account exists
|
|
57
|
+
if (!account) {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: 'Account not found or access denied' },
|
|
60
|
+
{ status: 404 }
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Verify account is active
|
|
65
|
+
if (!account.isActive) {
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{
|
|
68
|
+
error: 'Account already disconnected',
|
|
69
|
+
message: 'This account is already inactive.',
|
|
70
|
+
},
|
|
71
|
+
{ status: 400 }
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. Mark account as inactive (soft delete)
|
|
76
|
+
await mutateWithRLS(
|
|
77
|
+
`UPDATE "social_accounts"
|
|
78
|
+
SET "isActive" = false, "updatedAt" = CURRENT_TIMESTAMP
|
|
79
|
+
WHERE id = $1 AND "userId" = $2`,
|
|
80
|
+
[accountId, authResult.user!.id],
|
|
81
|
+
authResult.user!.id
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
console.log('[social-disconnect] ✅ Account marked as inactive:', {
|
|
85
|
+
accountId,
|
|
86
|
+
userId: authResult.user!.id,
|
|
87
|
+
platform: account.platform,
|
|
88
|
+
accountName: account.username,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// 5. Create audit log
|
|
92
|
+
await mutateWithRLS(
|
|
93
|
+
`INSERT INTO "audit_logs"
|
|
94
|
+
("userId", "accountId", action, details, "ipAddress", "userAgent")
|
|
95
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
96
|
+
[
|
|
97
|
+
authResult.user!.id,
|
|
98
|
+
accountId,
|
|
99
|
+
'account_disconnected',
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
platform: account.platform,
|
|
102
|
+
accountName: account.username,
|
|
103
|
+
success: true,
|
|
104
|
+
disconnectedAt: new Date().toISOString()
|
|
105
|
+
}),
|
|
106
|
+
request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null,
|
|
107
|
+
request.headers.get('user-agent') || null
|
|
108
|
+
],
|
|
109
|
+
authResult.user!.id
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
console.log('[social-disconnect] ✅ Audit log created:', {
|
|
113
|
+
action: 'account_disconnected',
|
|
114
|
+
platform: account.platform,
|
|
115
|
+
success: true,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// 6. Return success
|
|
119
|
+
return NextResponse.json({
|
|
120
|
+
success: true,
|
|
121
|
+
accountId,
|
|
122
|
+
message: `Successfully disconnected ${account.username} (${account.platform})`,
|
|
123
|
+
})
|
|
124
|
+
} catch (error: unknown) {
|
|
125
|
+
console.error('❌ Social disconnect error:', error)
|
|
126
|
+
|
|
127
|
+
return NextResponse.json(
|
|
128
|
+
{
|
|
129
|
+
error: 'Failed to disconnect account',
|
|
130
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
131
|
+
},
|
|
132
|
+
{ status: 500 }
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* DELETE method - Alternative endpoint using accountId in URL
|
|
139
|
+
*/
|
|
140
|
+
export async function DELETE(
|
|
141
|
+
request: NextRequest,
|
|
142
|
+
{ params }: { params: Promise<{ accountId: string }> }
|
|
143
|
+
) {
|
|
144
|
+
try {
|
|
145
|
+
// 1. Authentication
|
|
146
|
+
const authResult = await authenticateRequest(request)
|
|
147
|
+
if (!authResult.success) {
|
|
148
|
+
return NextResponse.json(
|
|
149
|
+
{ error: 'Authentication required' },
|
|
150
|
+
{ status: 401 }
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const resolvedParams = await params
|
|
155
|
+
const accountId = resolvedParams.accountId
|
|
156
|
+
|
|
157
|
+
// Validate UUID format
|
|
158
|
+
const uuidRegex =
|
|
159
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
160
|
+
if (!uuidRegex.test(accountId)) {
|
|
161
|
+
return NextResponse.json(
|
|
162
|
+
{ error: 'Invalid account ID format' },
|
|
163
|
+
{ status: 400 }
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Reuse POST logic
|
|
168
|
+
const body = { accountId }
|
|
169
|
+
return POST(
|
|
170
|
+
new NextRequest(request.url, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: request.headers,
|
|
173
|
+
body: JSON.stringify(body),
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
} catch (error: unknown) {
|
|
177
|
+
console.error('❌ Social disconnect DELETE error:', error)
|
|
178
|
+
|
|
179
|
+
return NextResponse.json(
|
|
180
|
+
{
|
|
181
|
+
error: 'Failed to disconnect account',
|
|
182
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
183
|
+
},
|
|
184
|
+
{ status: 500 }
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|