@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,646 @@
|
|
|
1
|
+
# Audit Logging
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Social Media Publisher plugin implements a comprehensive, **immutable audit trail** that logs every significant action performed through the system. This provides complete visibility for compliance, debugging, billing, and security monitoring.
|
|
6
|
+
|
|
7
|
+
**Key Characteristics:**
|
|
8
|
+
- **Immutable** - Logs cannot be modified or deleted
|
|
9
|
+
- **Complete** - Every action logged with full context
|
|
10
|
+
- **Attributed** - User, account, IP, and User-Agent tracking
|
|
11
|
+
- **Timestamped** - Precise timestamp for all events
|
|
12
|
+
- **GDPR-Compliant** - Personal data handling follows regulations
|
|
13
|
+
|
|
14
|
+
## Audit Logs Entity
|
|
15
|
+
|
|
16
|
+
### Database Schema
|
|
17
|
+
|
|
18
|
+
```sql
|
|
19
|
+
CREATE TABLE "audit_logs" (
|
|
20
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
21
|
+
"userId" TEXT NOT NULL, -- Who performed the action
|
|
22
|
+
"accountId" UUID, -- Which social account (optional)
|
|
23
|
+
action TEXT NOT NULL, -- What happened
|
|
24
|
+
details JSONB DEFAULT '{}', -- Full context
|
|
25
|
+
"ipAddress" TEXT, -- IP address of request
|
|
26
|
+
"userAgent" TEXT, -- Browser/client info
|
|
27
|
+
"createdAt" TIMESTAMPTZ DEFAULT now() -- When it happened
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- Indexes for common queries
|
|
31
|
+
CREATE INDEX "idx_audit_logs_userId" ON "audit_logs"("userId");
|
|
32
|
+
CREATE INDEX "idx_audit_logs_accountId" ON "audit_logs"("accountId");
|
|
33
|
+
CREATE INDEX "idx_audit_logs_action" ON "audit_logs"(action);
|
|
34
|
+
CREATE INDEX "idx_audit_logs_createdAt" ON "audit_logs"("createdAt" DESC);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Field Descriptions
|
|
38
|
+
|
|
39
|
+
**id** (UUID)
|
|
40
|
+
- Unique identifier for log entry
|
|
41
|
+
- Auto-generated
|
|
42
|
+
- Used for reference and deduplication
|
|
43
|
+
|
|
44
|
+
**userId** (TEXT)
|
|
45
|
+
- User who performed the action
|
|
46
|
+
- References `users.id`
|
|
47
|
+
- Never null (system user for automated actions)
|
|
48
|
+
|
|
49
|
+
**accountId** (UUID, nullable)
|
|
50
|
+
- Social media account involved
|
|
51
|
+
- References `clients_social_platforms.id`
|
|
52
|
+
- Null for actions not tied to specific account
|
|
53
|
+
|
|
54
|
+
**action** (TEXT)
|
|
55
|
+
- Type of action performed
|
|
56
|
+
- Standardized enum values
|
|
57
|
+
- Used for filtering and reporting
|
|
58
|
+
|
|
59
|
+
**details** (JSONB)
|
|
60
|
+
- Full context of the action
|
|
61
|
+
- Flexible structure
|
|
62
|
+
- Includes relevant metadata
|
|
63
|
+
|
|
64
|
+
**ipAddress** (TEXT, nullable)
|
|
65
|
+
- IP address of request origin
|
|
66
|
+
- Useful for security monitoring
|
|
67
|
+
- Null for system-initiated actions
|
|
68
|
+
|
|
69
|
+
**userAgent** (TEXT, nullable)
|
|
70
|
+
- Browser/client information
|
|
71
|
+
- Helps identify automation vs human
|
|
72
|
+
- Null for backend processes
|
|
73
|
+
|
|
74
|
+
**createdAt** (TIMESTAMPTZ)
|
|
75
|
+
- Timestamp of action
|
|
76
|
+
- Automatically set
|
|
77
|
+
- Used for chronological sorting and retention
|
|
78
|
+
|
|
79
|
+
## Tracked Actions
|
|
80
|
+
|
|
81
|
+
### Account Management Actions
|
|
82
|
+
|
|
83
|
+
#### `account_connected`
|
|
84
|
+
|
|
85
|
+
**When:** User successfully connects social media account via OAuth
|
|
86
|
+
|
|
87
|
+
**Details Structure:**
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"platform": "instagram_business",
|
|
91
|
+
"accountName": "@brandname",
|
|
92
|
+
"accountId": "17841401234567890",
|
|
93
|
+
"clientId": "550e8400-e29b-41d4-a716-446655440000",
|
|
94
|
+
"permissions": ["pages_show_list", "instagram_basic", "instagram_content_publish"],
|
|
95
|
+
"tokenExpiresAt": "2024-03-15T10:30:00Z"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Example:**
|
|
100
|
+
```typescript
|
|
101
|
+
await query(`
|
|
102
|
+
INSERT INTO "audit_logs" (
|
|
103
|
+
"userId", "accountId", action, details, "ipAddress", "userAgent"
|
|
104
|
+
) VALUES ($1, $2, $3, $4, $5, $6)
|
|
105
|
+
`, [
|
|
106
|
+
userId,
|
|
107
|
+
accountId,
|
|
108
|
+
'account_connected',
|
|
109
|
+
JSON.stringify({
|
|
110
|
+
platform: 'instagram_business',
|
|
111
|
+
accountName: '@brandname',
|
|
112
|
+
clientId: clientId
|
|
113
|
+
}),
|
|
114
|
+
request.headers.get('x-forwarded-for'),
|
|
115
|
+
request.headers.get('user-agent')
|
|
116
|
+
])
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### `account_disconnected`
|
|
120
|
+
|
|
121
|
+
**When:** User removes social media account
|
|
122
|
+
|
|
123
|
+
**Details Structure:**
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"platform": "facebook_page",
|
|
127
|
+
"accountName": "My Business Page",
|
|
128
|
+
"reason": "user_requested",
|
|
129
|
+
"hadActiveTokens": true
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Publishing Actions
|
|
134
|
+
|
|
135
|
+
#### `post_published`
|
|
136
|
+
|
|
137
|
+
**When:** Content successfully published to social platform
|
|
138
|
+
|
|
139
|
+
**Details Structure:**
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"platform": "instagram_business",
|
|
143
|
+
"accountName": "@brandname",
|
|
144
|
+
"postId": "17899618652010220",
|
|
145
|
+
"postUrl": "https://www.instagram.com/p/ABC123",
|
|
146
|
+
"imageUrl": "https://cdn.example.com/image.jpg",
|
|
147
|
+
"caption": "My awesome post! #instagram",
|
|
148
|
+
"captionLength": 35,
|
|
149
|
+
"publishedAt": "2024-01-15T14:30:00Z",
|
|
150
|
+
"success": true
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Example:**
|
|
155
|
+
```typescript
|
|
156
|
+
await query(`
|
|
157
|
+
INSERT INTO "audit_logs" (
|
|
158
|
+
"userId", "accountId", action, details, "ipAddress", "userAgent"
|
|
159
|
+
) VALUES ($1, $2::uuid, $3, $4, $5, $6)
|
|
160
|
+
`, [
|
|
161
|
+
userId,
|
|
162
|
+
accountId,
|
|
163
|
+
'post_published',
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
platform: 'instagram_business',
|
|
166
|
+
accountName: account.platformAccountName,
|
|
167
|
+
postId: result.postId,
|
|
168
|
+
postUrl: result.postUrl,
|
|
169
|
+
imageUrl: imageUrl,
|
|
170
|
+
caption: caption || '',
|
|
171
|
+
publishedAt: new Date().toISOString(),
|
|
172
|
+
success: true
|
|
173
|
+
}),
|
|
174
|
+
request.headers.get('x-forwarded-for'),
|
|
175
|
+
request.headers.get('user-agent')
|
|
176
|
+
])
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### `post_failed`
|
|
180
|
+
|
|
181
|
+
**When:** Publishing attempt fails
|
|
182
|
+
|
|
183
|
+
**Details Structure:**
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"platform": "facebook_page",
|
|
187
|
+
"accountName": "My Business Page",
|
|
188
|
+
"error": "Invalid parameter",
|
|
189
|
+
"errorCode": 100,
|
|
190
|
+
"errorDetails": {
|
|
191
|
+
"message": "Image URL must be publicly accessible",
|
|
192
|
+
"type": "OAuthException"
|
|
193
|
+
},
|
|
194
|
+
"imageUrl": "https://private.example.com/image.jpg",
|
|
195
|
+
"caption": "Failed post caption",
|
|
196
|
+
"attemptedAt": "2024-01-15T14:30:00Z"
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Token Management Actions
|
|
201
|
+
|
|
202
|
+
#### `token_refreshed`
|
|
203
|
+
|
|
204
|
+
**When:** OAuth token automatically refreshed
|
|
205
|
+
|
|
206
|
+
**Details Structure:**
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"platform": "instagram_business",
|
|
210
|
+
"accountName": "@brandname",
|
|
211
|
+
"oldTokenExpiresAt": "2024-01-20T10:30:00Z",
|
|
212
|
+
"newTokenExpiresAt": "2024-03-21T10:30:00Z",
|
|
213
|
+
"minutesBeforeExpiry": 8,
|
|
214
|
+
"refreshMethod": "automatic"
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Example:**
|
|
219
|
+
```typescript
|
|
220
|
+
await query(`
|
|
221
|
+
INSERT INTO "audit_logs" (
|
|
222
|
+
"userId", "accountId", action, details
|
|
223
|
+
) VALUES ($1, $2, $3, $4)
|
|
224
|
+
`, [
|
|
225
|
+
'system', // Automated action
|
|
226
|
+
accountId,
|
|
227
|
+
'token_refreshed',
|
|
228
|
+
JSON.stringify({
|
|
229
|
+
platform: account.platform,
|
|
230
|
+
accountName: account.platformAccountName,
|
|
231
|
+
oldTokenExpiresAt: oldExpiresAt,
|
|
232
|
+
newTokenExpiresAt: newExpiresAt,
|
|
233
|
+
minutesBeforeExpiry: minutesLeft,
|
|
234
|
+
refreshMethod: 'automatic'
|
|
235
|
+
})
|
|
236
|
+
])
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### `token_refresh_failed`
|
|
240
|
+
|
|
241
|
+
**When:** Token refresh attempt fails
|
|
242
|
+
|
|
243
|
+
**Details Structure:**
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"platform": "facebook_page",
|
|
247
|
+
"accountName": "My Business Page",
|
|
248
|
+
"error": "Invalid OAuth 2.0 Access Token",
|
|
249
|
+
"tokenExpiresAt": "2024-01-15T10:30:00Z",
|
|
250
|
+
"minutesBeforeExpiry": 5,
|
|
251
|
+
"requiresReconnection": true
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Querying Audit Logs
|
|
256
|
+
|
|
257
|
+
### Get User's Activity
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
async function getUserActivity(
|
|
261
|
+
userId: string,
|
|
262
|
+
limit: number = 50
|
|
263
|
+
): Promise<AuditLog[]> {
|
|
264
|
+
const result = await query(`
|
|
265
|
+
SELECT * FROM "audit_logs"
|
|
266
|
+
WHERE "userId" = $1
|
|
267
|
+
ORDER BY "createdAt" DESC
|
|
268
|
+
LIMIT $2
|
|
269
|
+
`, [userId, limit])
|
|
270
|
+
|
|
271
|
+
return result.rows
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Get Account Activity
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
async function getAccountActivity(
|
|
279
|
+
accountId: string
|
|
280
|
+
): Promise<AuditLog[]> {
|
|
281
|
+
const result = await query(`
|
|
282
|
+
SELECT * FROM "audit_logs"
|
|
283
|
+
WHERE "accountId" = $1
|
|
284
|
+
ORDER BY "createdAt" DESC
|
|
285
|
+
`, [accountId])
|
|
286
|
+
|
|
287
|
+
return result.rows
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Get Failed Publish Attempts
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
async function getFailedPublishAttempts(
|
|
295
|
+
userId: string,
|
|
296
|
+
days: number = 30
|
|
297
|
+
): Promise<AuditLog[]> {
|
|
298
|
+
const result = await query(`
|
|
299
|
+
SELECT * FROM "audit_logs"
|
|
300
|
+
WHERE "userId" = $1
|
|
301
|
+
AND action = 'post_failed'
|
|
302
|
+
AND "createdAt" > NOW() - INTERVAL '${days} days'
|
|
303
|
+
ORDER BY "createdAt" DESC
|
|
304
|
+
`, [userId])
|
|
305
|
+
|
|
306
|
+
return result.rows
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Get Token Refresh History
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
async function getTokenRefreshHistory(
|
|
314
|
+
accountId: string
|
|
315
|
+
): Promise<AuditLog[]> {
|
|
316
|
+
const result = await query(`
|
|
317
|
+
SELECT * FROM "audit_logs"
|
|
318
|
+
WHERE "accountId" = $1
|
|
319
|
+
AND action IN ('token_refreshed', 'token_refresh_failed')
|
|
320
|
+
ORDER BY "createdAt" DESC
|
|
321
|
+
`, [accountId])
|
|
322
|
+
|
|
323
|
+
return result.rows
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Activity By Action Type
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
async function getActivityByAction(
|
|
331
|
+
userId: string,
|
|
332
|
+
action: string,
|
|
333
|
+
startDate: Date,
|
|
334
|
+
endDate: Date
|
|
335
|
+
): Promise<AuditLog[]> {
|
|
336
|
+
const result = await query(`
|
|
337
|
+
SELECT * FROM "audit_logs"
|
|
338
|
+
WHERE "userId" = $1
|
|
339
|
+
AND action = $2
|
|
340
|
+
AND "createdAt" BETWEEN $3 AND $4
|
|
341
|
+
ORDER BY "createdAt" DESC
|
|
342
|
+
`, [userId, action, startDate, endDate])
|
|
343
|
+
|
|
344
|
+
return result.rows
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Reporting and Analytics
|
|
349
|
+
|
|
350
|
+
### Publishing Success Rate
|
|
351
|
+
|
|
352
|
+
```sql
|
|
353
|
+
SELECT
|
|
354
|
+
DATE("createdAt") as date,
|
|
355
|
+
COUNT(*) FILTER (WHERE action = 'post_published') as successful,
|
|
356
|
+
COUNT(*) FILTER (WHERE action = 'post_failed') as failed,
|
|
357
|
+
ROUND(
|
|
358
|
+
COUNT(*) FILTER (WHERE action = 'post_published')::decimal /
|
|
359
|
+
NULLIF(COUNT(*), 0) * 100,
|
|
360
|
+
2
|
|
361
|
+
) as success_rate
|
|
362
|
+
FROM "audit_logs"
|
|
363
|
+
WHERE action IN ('post_published', 'post_failed')
|
|
364
|
+
AND "createdAt" > NOW() - INTERVAL '30 days'
|
|
365
|
+
GROUP BY DATE("createdAt")
|
|
366
|
+
ORDER BY date DESC;
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Most Active Users
|
|
370
|
+
|
|
371
|
+
```sql
|
|
372
|
+
SELECT
|
|
373
|
+
"userId",
|
|
374
|
+
COUNT(*) FILTER (WHERE action = 'post_published') as posts_published,
|
|
375
|
+
COUNT(*) FILTER (WHERE action = 'account_connected') as accounts_connected,
|
|
376
|
+
COUNT(*) as total_actions
|
|
377
|
+
FROM "audit_logs"
|
|
378
|
+
WHERE "createdAt" > NOW() - INTERVAL '30 days'
|
|
379
|
+
GROUP BY "userId"
|
|
380
|
+
ORDER BY total_actions DESC
|
|
381
|
+
LIMIT 10;
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Platform Distribution
|
|
385
|
+
|
|
386
|
+
```sql
|
|
387
|
+
SELECT
|
|
388
|
+
details->>'platform' as platform,
|
|
389
|
+
COUNT(*) as publish_count
|
|
390
|
+
FROM "audit_logs"
|
|
391
|
+
WHERE action = 'post_published'
|
|
392
|
+
AND "createdAt" > NOW() - INTERVAL '30 days'
|
|
393
|
+
GROUP BY details->>'platform'
|
|
394
|
+
ORDER BY publish_count DESC;
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Error Analysis
|
|
398
|
+
|
|
399
|
+
```sql
|
|
400
|
+
SELECT
|
|
401
|
+
details->>'error' as error_message,
|
|
402
|
+
details->>'platform' as platform,
|
|
403
|
+
COUNT(*) as occurrence_count,
|
|
404
|
+
MAX("createdAt") as last_occurred
|
|
405
|
+
FROM "audit_logs"
|
|
406
|
+
WHERE action = 'post_failed'
|
|
407
|
+
AND "createdAt" > NOW() - INTERVAL '7 days'
|
|
408
|
+
GROUP BY details->>'error', details->>'platform'
|
|
409
|
+
ORDER BY occurrence_count DESC;
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Compliance and Retention
|
|
413
|
+
|
|
414
|
+
### GDPR Compliance
|
|
415
|
+
|
|
416
|
+
**Right to Access:**
|
|
417
|
+
```typescript
|
|
418
|
+
async function exportUserAuditLogs(userId: string): Promise<AuditLog[]> {
|
|
419
|
+
const result = await query(`
|
|
420
|
+
SELECT * FROM "audit_logs"
|
|
421
|
+
WHERE "userId" = $1
|
|
422
|
+
ORDER BY "createdAt" DESC
|
|
423
|
+
`, [userId])
|
|
424
|
+
|
|
425
|
+
return result.rows
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Right to Erasure:**
|
|
430
|
+
```typescript
|
|
431
|
+
async function anonymizeUserAuditLogs(userId: string): Promise<void> {
|
|
432
|
+
// Anonymize personal data while keeping audit trail
|
|
433
|
+
await query(`
|
|
434
|
+
UPDATE "audit_logs"
|
|
435
|
+
SET "userId" = 'anonymized',
|
|
436
|
+
"ipAddress" = NULL,
|
|
437
|
+
"userAgent" = NULL,
|
|
438
|
+
details = jsonb_set(
|
|
439
|
+
details,
|
|
440
|
+
'{userId}',
|
|
441
|
+
'"anonymized"'
|
|
442
|
+
)
|
|
443
|
+
WHERE "userId" = $1
|
|
444
|
+
`, [userId])
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Retention Policy
|
|
449
|
+
|
|
450
|
+
**Recommended Policy:**
|
|
451
|
+
- **Active Logs:** Keep 1 year online
|
|
452
|
+
- **Archive:** Move to cold storage after 1 year
|
|
453
|
+
- **Deletion:** Delete after 7 years (or per regulations)
|
|
454
|
+
|
|
455
|
+
**Archive Implementation:**
|
|
456
|
+
```sql
|
|
457
|
+
-- Create archive table
|
|
458
|
+
CREATE TABLE "audit_logs_archive" (
|
|
459
|
+
LIKE "audit_logs" INCLUDING ALL
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
-- Move old logs to archive
|
|
463
|
+
INSERT INTO "audit_logs_archive"
|
|
464
|
+
SELECT * FROM "audit_logs"
|
|
465
|
+
WHERE "createdAt" < NOW() - INTERVAL '1 year';
|
|
466
|
+
|
|
467
|
+
-- Delete from main table
|
|
468
|
+
DELETE FROM "audit_logs"
|
|
469
|
+
WHERE "createdAt" < NOW() - INTERVAL '1 year';
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Automated Retention:**
|
|
473
|
+
```typescript
|
|
474
|
+
// Run monthly via cron
|
|
475
|
+
async function enforceRetentionPolicy(): Promise<void> {
|
|
476
|
+
// Archive logs older than 1 year
|
|
477
|
+
await query(`
|
|
478
|
+
INSERT INTO "audit_logs_archive"
|
|
479
|
+
SELECT * FROM "audit_logs"
|
|
480
|
+
WHERE "createdAt" < NOW() - INTERVAL '1 year'
|
|
481
|
+
`)
|
|
482
|
+
|
|
483
|
+
// Delete archived logs
|
|
484
|
+
await query(`
|
|
485
|
+
DELETE FROM "audit_logs"
|
|
486
|
+
WHERE "createdAt" < NOW() - INTERVAL '1 year'
|
|
487
|
+
`)
|
|
488
|
+
|
|
489
|
+
// Delete archive logs older than 7 years
|
|
490
|
+
await query(`
|
|
491
|
+
DELETE FROM "audit_logs_archive"
|
|
492
|
+
WHERE "createdAt" < NOW() - INTERVAL '7 years'
|
|
493
|
+
`)
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
## Monitoring and Alerting
|
|
498
|
+
|
|
499
|
+
### Key Metrics to Monitor
|
|
500
|
+
|
|
501
|
+
**1. Failed Publish Rate:**
|
|
502
|
+
```sql
|
|
503
|
+
SELECT
|
|
504
|
+
ROUND(
|
|
505
|
+
COUNT(*) FILTER (WHERE action = 'post_failed')::decimal /
|
|
506
|
+
NULLIF(COUNT(*), 0) * 100,
|
|
507
|
+
2
|
|
508
|
+
) as failed_rate
|
|
509
|
+
FROM "audit_logs"
|
|
510
|
+
WHERE action IN ('post_published', 'post_failed')
|
|
511
|
+
AND "createdAt" > NOW() - INTERVAL '1 hour';
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**Alert If:** Failed rate > 10%
|
|
515
|
+
|
|
516
|
+
**2. Token Refresh Failures:**
|
|
517
|
+
```sql
|
|
518
|
+
SELECT COUNT(*) as failures
|
|
519
|
+
FROM "audit_logs"
|
|
520
|
+
WHERE action = 'token_refresh_failed'
|
|
521
|
+
AND "createdAt" > NOW() - INTERVAL '1 day';
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**Alert If:** > 5 failures per day
|
|
525
|
+
|
|
526
|
+
**3. Unusual Activity:**
|
|
527
|
+
```sql
|
|
528
|
+
-- Detect accounts with unusually high publish volume
|
|
529
|
+
SELECT
|
|
530
|
+
"accountId",
|
|
531
|
+
COUNT(*) as publish_count
|
|
532
|
+
FROM "audit_logs"
|
|
533
|
+
WHERE action = 'post_published'
|
|
534
|
+
AND "createdAt" > NOW() - INTERVAL '1 hour'
|
|
535
|
+
GROUP BY "accountId"
|
|
536
|
+
HAVING COUNT(*) > 10; -- More than 10 posts per hour
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Alert If:** Unusual spikes detected (possible abuse)
|
|
540
|
+
|
|
541
|
+
### Alerting Implementation
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
async function checkAuditAlertsAndNotify(): Promise<void> {
|
|
545
|
+
// Check failed publish rate
|
|
546
|
+
const failedRate = await getFailedPublishRate()
|
|
547
|
+
if (failedRate > 10) {
|
|
548
|
+
await sendAlert('high-failed-rate', { rate: failedRate })
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Check token refresh failures
|
|
552
|
+
const tokenFailures = await getTokenRefreshFailures()
|
|
553
|
+
if (tokenFailures > 5) {
|
|
554
|
+
await sendAlert('token-refresh-failures', { count: tokenFailures })
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Check for unusual activity
|
|
558
|
+
const unusualAccounts = await getUnusualActivity()
|
|
559
|
+
if (unusualAccounts.length > 0) {
|
|
560
|
+
await sendAlert('unusual-activity', { accounts: unusualAccounts })
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Best Practices
|
|
566
|
+
|
|
567
|
+
### Do's ✅
|
|
568
|
+
|
|
569
|
+
**1. Always Log Critical Actions:**
|
|
570
|
+
```typescript
|
|
571
|
+
// ✅ Good: Log every publish attempt
|
|
572
|
+
await logAudit('post_published', { ... })
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**2. Include Full Context:**
|
|
576
|
+
```typescript
|
|
577
|
+
// ✅ Good: Rich details
|
|
578
|
+
details: {
|
|
579
|
+
platform: 'instagram',
|
|
580
|
+
accountName: '@brand',
|
|
581
|
+
postId: '123',
|
|
582
|
+
imageUrl: 'https://...',
|
|
583
|
+
caption: '...',
|
|
584
|
+
success: true
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ❌ Bad: Minimal details
|
|
588
|
+
details: { success: true }
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**3. Capture IP and User-Agent:**
|
|
592
|
+
```typescript
|
|
593
|
+
// ✅ Good
|
|
594
|
+
ipAddress: request.headers.get('x-forwarded-for'),
|
|
595
|
+
userAgent: request.headers.get('user-agent')
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**4. Use Consistent Action Names:**
|
|
599
|
+
```typescript
|
|
600
|
+
// ✅ Good: Standardized
|
|
601
|
+
'account_connected', 'post_published', 'token_refreshed'
|
|
602
|
+
|
|
603
|
+
// ❌ Bad: Inconsistent
|
|
604
|
+
'ACCOUNT_CONNECTED', 'post-published', 'Token Refreshed'
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Don'ts ❌
|
|
608
|
+
|
|
609
|
+
**1. Don't Log Sensitive Data:**
|
|
610
|
+
```typescript
|
|
611
|
+
// ❌ Bad: Logs token
|
|
612
|
+
details: { token: decryptedToken }
|
|
613
|
+
|
|
614
|
+
// ✅ Good: Never log tokens
|
|
615
|
+
details: { tokenRefreshed: true }
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
**2. Don't Allow Log Deletion:**
|
|
619
|
+
```sql
|
|
620
|
+
-- ❌ Bad: DELETE operations allowed
|
|
621
|
+
DELETE FROM "audit_logs" WHERE id = $1;
|
|
622
|
+
|
|
623
|
+
-- ✅ Good: No DELETE, only INSERT
|
|
624
|
+
-- Enforce via database permissions
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**3. Don't Skip Error Logging:**
|
|
628
|
+
```typescript
|
|
629
|
+
// ❌ Bad
|
|
630
|
+
if (error) {
|
|
631
|
+
console.error(error)
|
|
632
|
+
return // No audit log
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ✅ Good
|
|
636
|
+
if (error) {
|
|
637
|
+
await logAudit('post_failed', { error: error.message })
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## Next Steps
|
|
643
|
+
|
|
644
|
+
- **[Publishing](./02-publishing.md)** - See how audit logging integrates with publishing
|
|
645
|
+
- **[Token Management](./03-token-management.md)** - Token refresh audit logs
|
|
646
|
+
- **[Per-Client Architecture](../03-advanced-usage/03-per-client-architecture.md)** - Understand data model
|