@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Logs Entity Configuration
|
|
3
|
+
*
|
|
4
|
+
* Tracks all actions performed through the social media plugin
|
|
5
|
+
* For security, compliance, and debugging
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EntityConfig } from '@nextsparkjs/core/lib/entities/types'
|
|
9
|
+
|
|
10
|
+
export const auditLogsEntityConfig: any = {
|
|
11
|
+
name: 'audit-logs',
|
|
12
|
+
label: {
|
|
13
|
+
en: 'Social Media Audit Logs',
|
|
14
|
+
es: 'Registros de Auditoría de Redes Sociales'
|
|
15
|
+
},
|
|
16
|
+
description: {
|
|
17
|
+
en: 'Security and audit trail for social media actions',
|
|
18
|
+
es: 'Pista de auditoría y seguridad para acciones de redes sociales'
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
fields: [
|
|
22
|
+
{
|
|
23
|
+
name: 'user_id',
|
|
24
|
+
label: { en: 'User', es: 'Usuario' },
|
|
25
|
+
type: 'relation',
|
|
26
|
+
relation: {
|
|
27
|
+
entity: 'users',
|
|
28
|
+
titleField: 'email'
|
|
29
|
+
},
|
|
30
|
+
required: true,
|
|
31
|
+
index: true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'account_id',
|
|
35
|
+
label: { en: 'Social Platform Account', es: 'Cuenta de Plataforma Social' },
|
|
36
|
+
type: 'relation',
|
|
37
|
+
relation: {
|
|
38
|
+
entity: 'social-platforms',
|
|
39
|
+
titleField: 'username'
|
|
40
|
+
},
|
|
41
|
+
required: false,
|
|
42
|
+
index: true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'action',
|
|
46
|
+
label: { en: 'Action', es: 'Acción' },
|
|
47
|
+
type: 'select',
|
|
48
|
+
options: [
|
|
49
|
+
{ value: 'account_connected', label: 'Account Connected' },
|
|
50
|
+
{ value: 'account_disconnected', label: 'Account Disconnected' },
|
|
51
|
+
{ value: 'post_published', label: 'Post Published' },
|
|
52
|
+
{ value: 'post_failed', label: 'Post Failed' },
|
|
53
|
+
{ value: 'token_refreshed', label: 'Token Refreshed' },
|
|
54
|
+
{ value: 'token_refresh_failed', label: 'Token Refresh Failed' }
|
|
55
|
+
],
|
|
56
|
+
required: true,
|
|
57
|
+
index: true
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'details',
|
|
61
|
+
label: { en: 'Details', es: 'Detalles' },
|
|
62
|
+
type: 'json',
|
|
63
|
+
default: {},
|
|
64
|
+
description: {
|
|
65
|
+
en: 'Action details: platform, success status, error messages, metadata',
|
|
66
|
+
es: 'Detalles de acción: plataforma, estado de éxito, mensajes de error, metadatos'
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'ip_address',
|
|
71
|
+
label: { en: 'IP Address', es: 'Dirección IP' },
|
|
72
|
+
type: 'string',
|
|
73
|
+
required: false,
|
|
74
|
+
description: {
|
|
75
|
+
en: 'User IP address at time of action',
|
|
76
|
+
es: 'Dirección IP del usuario al momento de la acción'
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'user_agent',
|
|
81
|
+
label: { en: 'User Agent', es: 'Agente de Usuario' },
|
|
82
|
+
type: 'string',
|
|
83
|
+
required: false,
|
|
84
|
+
description: {
|
|
85
|
+
en: 'Browser/device user agent string',
|
|
86
|
+
es: 'String de agente de usuario del navegador/dispositivo'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
|
|
91
|
+
// Database indexes for performance
|
|
92
|
+
indexes: [
|
|
93
|
+
{
|
|
94
|
+
fields: ['user_id', 'created_at'],
|
|
95
|
+
name: 'idx_audit_logs_user_created'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
fields: ['account_id', 'action'],
|
|
99
|
+
name: 'idx_audit_logs_account_action'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
fields: ['action', 'created_at'],
|
|
103
|
+
name: 'idx_audit_logs_action_created'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
fields: ['created_at'],
|
|
107
|
+
name: 'idx_audit_logs_created',
|
|
108
|
+
order: 'DESC' // Most recent first
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
|
|
112
|
+
// Row-level security
|
|
113
|
+
permissions: {
|
|
114
|
+
actions: [
|
|
115
|
+
{
|
|
116
|
+
action: 'create',
|
|
117
|
+
label: 'Create audit logs',
|
|
118
|
+
description: 'Can create audit log entries (typically system-only)',
|
|
119
|
+
defaultRoles: [], // Only system creates logs, no user role has this permission
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
action: 'read',
|
|
123
|
+
label: 'View audit logs',
|
|
124
|
+
description: 'Can view audit log entries',
|
|
125
|
+
defaultRoles: ['owner', 'admin'],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
action: 'list',
|
|
129
|
+
label: 'List audit logs',
|
|
130
|
+
description: 'Can list audit log entries',
|
|
131
|
+
defaultRoles: ['owner', 'admin'],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
action: 'update',
|
|
135
|
+
label: 'Edit audit logs',
|
|
136
|
+
description: 'Audit logs are immutable - no one can update',
|
|
137
|
+
defaultRoles: [], // Immutable - empty array
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
action: 'delete',
|
|
141
|
+
label: 'Delete audit logs',
|
|
142
|
+
description: 'Can delete old audit log entries',
|
|
143
|
+
defaultRoles: ['owner', 'admin'],
|
|
144
|
+
dangerous: true,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default auditLogsEntityConfig
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Helper for Social Media Publishing
|
|
3
|
+
*
|
|
4
|
+
* Handles OAuth flow for connecting social accounts (NOT for authentication)
|
|
5
|
+
* This is separate from Better Auth providers which are for user login
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - Facebook OAuth (for Facebook Pages)
|
|
9
|
+
* - Instagram Graph API (via Facebook Pages - requires Instagram Business Account linked to Page)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Facebook OAuth endpoints (for Facebook Page + Instagram Graph API publishing)
|
|
13
|
+
const FACEBOOK_OAUTH_URL = 'https://www.facebook.com/v18.0/dialog/oauth'
|
|
14
|
+
const FACEBOOK_TOKEN_URL = 'https://graph.facebook.com/v18.0/oauth/access_token'
|
|
15
|
+
|
|
16
|
+
export interface OAuthConfig {
|
|
17
|
+
// Facebook OAuth credentials (used for both Facebook Pages and Instagram Graph API)
|
|
18
|
+
facebookClientId: string
|
|
19
|
+
facebookClientSecret: string
|
|
20
|
+
// Shared redirect URI
|
|
21
|
+
redirectUri: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate OAuth authorization URL for Facebook/Instagram publishing
|
|
26
|
+
*
|
|
27
|
+
* Both facebook_page and instagram_business use Facebook OAuth
|
|
28
|
+
* Instagram Graph API requires Facebook Pages with Instagram Business Account linked
|
|
29
|
+
*
|
|
30
|
+
* @param platform - 'facebook_page' or 'instagram_business'
|
|
31
|
+
* @param config - OAuth configuration
|
|
32
|
+
* @param state - CSRF protection state (store in session)
|
|
33
|
+
* @returns Authorization URL to redirect user to
|
|
34
|
+
*/
|
|
35
|
+
export function generateAuthorizationUrl(
|
|
36
|
+
platform: 'facebook_page' | 'instagram_business',
|
|
37
|
+
config: OAuthConfig,
|
|
38
|
+
state: string
|
|
39
|
+
): string {
|
|
40
|
+
// Both platforms use Facebook OAuth
|
|
41
|
+
// instagram_business requires additional Instagram scopes
|
|
42
|
+
const scopes = [
|
|
43
|
+
'pages_show_list',
|
|
44
|
+
'pages_manage_posts',
|
|
45
|
+
'pages_read_engagement',
|
|
46
|
+
'read_insights',
|
|
47
|
+
'business_management', // Required to read instagram_business_account field from Pages
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
// Add Instagram scopes for Instagram Graph API
|
|
51
|
+
if (platform === 'instagram_business') {
|
|
52
|
+
scopes.push(
|
|
53
|
+
'instagram_basic',
|
|
54
|
+
'instagram_content_publish',
|
|
55
|
+
'instagram_manage_comments'
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const params = new URLSearchParams({
|
|
60
|
+
client_id: config.facebookClientId,
|
|
61
|
+
redirect_uri: config.redirectUri,
|
|
62
|
+
state,
|
|
63
|
+
scope: scopes.join(','),
|
|
64
|
+
response_type: 'code',
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return `${FACEBOOK_OAUTH_URL}?${params.toString()}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Exchange authorization code for access token
|
|
72
|
+
*
|
|
73
|
+
* Both facebook_page and instagram_business use Facebook OAuth endpoint
|
|
74
|
+
*
|
|
75
|
+
* @param code - Authorization code from callback
|
|
76
|
+
* @param config - OAuth configuration
|
|
77
|
+
* @param platform - Platform type (both use same endpoint now)
|
|
78
|
+
* @returns Access token data
|
|
79
|
+
*/
|
|
80
|
+
export async function exchangeCodeForToken(
|
|
81
|
+
code: string,
|
|
82
|
+
config: OAuthConfig,
|
|
83
|
+
platform: 'facebook_page' | 'instagram_business'
|
|
84
|
+
): Promise<{
|
|
85
|
+
accessToken: string
|
|
86
|
+
expiresIn: number
|
|
87
|
+
tokenType: string
|
|
88
|
+
userId?: string
|
|
89
|
+
}> {
|
|
90
|
+
// Both platforms use Facebook OAuth - GET with query params
|
|
91
|
+
const params = new URLSearchParams({
|
|
92
|
+
client_id: config.facebookClientId,
|
|
93
|
+
client_secret: config.facebookClientSecret,
|
|
94
|
+
redirect_uri: config.redirectUri,
|
|
95
|
+
code,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const response = await fetch(`${FACEBOOK_TOKEN_URL}?${params.toString()}`)
|
|
99
|
+
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const errorText = await response.text()
|
|
102
|
+
throw new Error(`Facebook token exchange failed: ${errorText}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const data = await response.json()
|
|
106
|
+
|
|
107
|
+
if (data.error) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Facebook OAuth error: ${data.error.message || data.error_description || data.error}`
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
accessToken: data.access_token,
|
|
115
|
+
expiresIn: data.expires_in || 3600,
|
|
116
|
+
tokenType: data.token_type || 'Bearer',
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get OAuth configuration from environment
|
|
122
|
+
*
|
|
123
|
+
* Only requires Facebook credentials as Instagram Graph API uses same app
|
|
124
|
+
*/
|
|
125
|
+
export function getOAuthConfig(): OAuthConfig {
|
|
126
|
+
const facebookClientId = process.env.FACEBOOK_CLIENT_ID?.trim() || process.env.FACEBOOK_APP_ID?.trim()
|
|
127
|
+
const facebookClientSecret = process.env.FACEBOOK_CLIENT_SECRET?.trim() || process.env.FACEBOOK_APP_SECRET?.trim()
|
|
128
|
+
const baseUrl = (process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:5173').trim()
|
|
129
|
+
|
|
130
|
+
if (!facebookClientId || !facebookClientSecret) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'FACEBOOK_CLIENT_ID (or FACEBOOK_APP_ID) and FACEBOOK_CLIENT_SECRET (or FACEBOOK_APP_SECRET) environment variables are required'
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
facebookClientId,
|
|
138
|
+
facebookClientSecret,
|
|
139
|
+
redirectUri: `${baseUrl}/api/v1/plugin/social-media-publisher/social/connect/callback`,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Generate secure random state for CSRF protection
|
|
145
|
+
*/
|
|
146
|
+
export function generateState(): string {
|
|
147
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
|
148
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
149
|
+
.join('')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate state parameter to prevent CSRF attacks
|
|
154
|
+
* State should be stored in session and compared here
|
|
155
|
+
*
|
|
156
|
+
* @param receivedState - State from OAuth callback
|
|
157
|
+
* @param sessionState - State stored in session
|
|
158
|
+
* @returns true if states match
|
|
159
|
+
*/
|
|
160
|
+
export function validateState(receivedState: string, sessionState?: string): boolean {
|
|
161
|
+
if (!sessionState) {
|
|
162
|
+
console.warn('[oauth-helper] No session state found for validation')
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return receivedState === sessionState
|
|
167
|
+
}
|