@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.
@@ -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
+ }