@girardmedia/bootspring 1.2.0 → 2.0.3
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/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
# Audit Logging Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for tracking user actions and system events.
|
|
4
|
-
|
|
5
|
-
## Audit Log Schema
|
|
6
|
-
|
|
7
|
-
```prisma
|
|
8
|
-
// prisma/schema.prisma
|
|
9
|
-
model AuditLog {
|
|
10
|
-
id String @id @default(cuid())
|
|
11
|
-
timestamp DateTime @default(now())
|
|
12
|
-
userId String?
|
|
13
|
-
user User? @relation(fields: [userId], references: [id])
|
|
14
|
-
action String // e.g., "user.create", "post.delete"
|
|
15
|
-
entityType String // e.g., "User", "Post"
|
|
16
|
-
entityId String?
|
|
17
|
-
oldValues Json?
|
|
18
|
-
newValues Json?
|
|
19
|
-
metadata Json? // IP, user agent, etc.
|
|
20
|
-
status String @default("success") // success, failure
|
|
21
|
-
errorMessage String?
|
|
22
|
-
|
|
23
|
-
@@index([userId])
|
|
24
|
-
@@index([entityType, entityId])
|
|
25
|
-
@@index([action])
|
|
26
|
-
@@index([timestamp])
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Audit Logger Service
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// lib/audit/logger.ts
|
|
34
|
-
import { prisma } from '@/lib/db'
|
|
35
|
-
import { headers } from 'next/headers'
|
|
36
|
-
|
|
37
|
-
interface AuditContext {
|
|
38
|
-
userId?: string
|
|
39
|
-
ip?: string
|
|
40
|
-
userAgent?: string
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface AuditEntry {
|
|
44
|
-
action: string
|
|
45
|
-
entityType: string
|
|
46
|
-
entityId?: string
|
|
47
|
-
oldValues?: Record<string, any>
|
|
48
|
-
newValues?: Record<string, any>
|
|
49
|
-
metadata?: Record<string, any>
|
|
50
|
-
status?: 'success' | 'failure'
|
|
51
|
-
errorMessage?: string
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class AuditLogger {
|
|
55
|
-
private context: AuditContext
|
|
56
|
-
|
|
57
|
-
constructor(context?: AuditContext) {
|
|
58
|
-
this.context = context ?? {}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
static fromRequest(): AuditLogger {
|
|
62
|
-
const headersList = headers()
|
|
63
|
-
return new AuditLogger({
|
|
64
|
-
ip: headersList.get('x-forwarded-for') ?? headersList.get('x-real-ip') ?? undefined,
|
|
65
|
-
userAgent: headersList.get('user-agent') ?? undefined
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
setUser(userId: string): this {
|
|
70
|
-
this.context.userId = userId
|
|
71
|
-
return this
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async log(entry: AuditEntry): Promise<void> {
|
|
75
|
-
await prisma.auditLog.create({
|
|
76
|
-
data: {
|
|
77
|
-
userId: this.context.userId,
|
|
78
|
-
action: entry.action,
|
|
79
|
-
entityType: entry.entityType,
|
|
80
|
-
entityId: entry.entityId,
|
|
81
|
-
oldValues: entry.oldValues,
|
|
82
|
-
newValues: entry.newValues,
|
|
83
|
-
status: entry.status ?? 'success',
|
|
84
|
-
errorMessage: entry.errorMessage,
|
|
85
|
-
metadata: {
|
|
86
|
-
...entry.metadata,
|
|
87
|
-
ip: this.context.ip,
|
|
88
|
-
userAgent: this.context.userAgent
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async logSuccess(entry: Omit<AuditEntry, 'status'>): Promise<void> {
|
|
95
|
-
await this.log({ ...entry, status: 'success' })
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async logFailure(
|
|
99
|
-
entry: Omit<AuditEntry, 'status'>,
|
|
100
|
-
error?: Error
|
|
101
|
-
): Promise<void> {
|
|
102
|
-
await this.log({
|
|
103
|
-
...entry,
|
|
104
|
-
status: 'failure',
|
|
105
|
-
errorMessage: error?.message
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Singleton for simple usage
|
|
111
|
-
export const auditLog = new AuditLogger()
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Audit Decorator/Wrapper
|
|
115
|
-
|
|
116
|
-
```typescript
|
|
117
|
-
// lib/audit/decorator.ts
|
|
118
|
-
import { AuditLogger } from './logger'
|
|
119
|
-
|
|
120
|
-
interface AuditOptions {
|
|
121
|
-
action: string
|
|
122
|
-
entityType: string
|
|
123
|
-
getEntityId?: (result: any) => string
|
|
124
|
-
getOldValues?: () => Promise<Record<string, any>>
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function withAudit<T extends (...args: any[]) => Promise<any>>(
|
|
128
|
-
fn: T,
|
|
129
|
-
options: AuditOptions,
|
|
130
|
-
getUserId: () => string | undefined
|
|
131
|
-
): T {
|
|
132
|
-
return (async (...args: Parameters<T>) => {
|
|
133
|
-
const logger = AuditLogger.fromRequest()
|
|
134
|
-
const userId = getUserId()
|
|
135
|
-
|
|
136
|
-
if (userId) {
|
|
137
|
-
logger.setUser(userId)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const oldValues = options.getOldValues
|
|
141
|
-
? await options.getOldValues()
|
|
142
|
-
: undefined
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const result = await fn(...args)
|
|
146
|
-
|
|
147
|
-
await logger.logSuccess({
|
|
148
|
-
action: options.action,
|
|
149
|
-
entityType: options.entityType,
|
|
150
|
-
entityId: options.getEntityId?.(result),
|
|
151
|
-
oldValues,
|
|
152
|
-
newValues: result
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
return result
|
|
156
|
-
} catch (error) {
|
|
157
|
-
await logger.logFailure(
|
|
158
|
-
{
|
|
159
|
-
action: options.action,
|
|
160
|
-
entityType: options.entityType,
|
|
161
|
-
oldValues
|
|
162
|
-
},
|
|
163
|
-
error as Error
|
|
164
|
-
)
|
|
165
|
-
throw error
|
|
166
|
-
}
|
|
167
|
-
}) as T
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Usage example
|
|
171
|
-
const createUserWithAudit = withAudit(
|
|
172
|
-
async (data: { email: string; name: string }) => {
|
|
173
|
-
return prisma.user.create({ data })
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
action: 'user.create',
|
|
177
|
-
entityType: 'User',
|
|
178
|
-
getEntityId: (user) => user.id
|
|
179
|
-
},
|
|
180
|
-
() => getCurrentUserId()
|
|
181
|
-
)
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
## Prisma Middleware for Automatic Auditing
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
// lib/audit/prisma-middleware.ts
|
|
188
|
-
import { Prisma } from '@prisma/client'
|
|
189
|
-
import { prisma } from '@/lib/db'
|
|
190
|
-
|
|
191
|
-
const AUDITED_MODELS = ['User', 'Post', 'Team', 'Subscription']
|
|
192
|
-
const AUDITED_ACTIONS = ['create', 'update', 'delete']
|
|
193
|
-
|
|
194
|
-
export const auditMiddleware: Prisma.Middleware = async (params, next) => {
|
|
195
|
-
const { model, action, args } = params
|
|
196
|
-
|
|
197
|
-
if (!model || !AUDITED_MODELS.includes(model)) {
|
|
198
|
-
return next(params)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!AUDITED_ACTIONS.includes(action)) {
|
|
202
|
-
return next(params)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Get old values for update/delete
|
|
206
|
-
let oldValues: any = null
|
|
207
|
-
if ((action === 'update' || action === 'delete') && args.where) {
|
|
208
|
-
oldValues = await (prisma as any)[model.toLowerCase()].findUnique({
|
|
209
|
-
where: args.where
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
try {
|
|
214
|
-
const result = await next(params)
|
|
215
|
-
|
|
216
|
-
// Log successful operation
|
|
217
|
-
await prisma.auditLog.create({
|
|
218
|
-
data: {
|
|
219
|
-
action: `${model.toLowerCase()}.${action}`,
|
|
220
|
-
entityType: model,
|
|
221
|
-
entityId: result?.id ?? args.where?.id,
|
|
222
|
-
oldValues: oldValues ? JSON.parse(JSON.stringify(oldValues)) : null,
|
|
223
|
-
newValues: action !== 'delete' ? JSON.parse(JSON.stringify(result)) : null,
|
|
224
|
-
status: 'success'
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
return result
|
|
229
|
-
} catch (error) {
|
|
230
|
-
// Log failed operation
|
|
231
|
-
await prisma.auditLog.create({
|
|
232
|
-
data: {
|
|
233
|
-
action: `${model.toLowerCase()}.${action}`,
|
|
234
|
-
entityType: model,
|
|
235
|
-
entityId: args.where?.id,
|
|
236
|
-
oldValues: oldValues ? JSON.parse(JSON.stringify(oldValues)) : null,
|
|
237
|
-
status: 'failure',
|
|
238
|
-
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
throw error
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Register middleware
|
|
247
|
-
// prisma.$use(auditMiddleware)
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
## API Route Auditing
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
// lib/audit/route-wrapper.ts
|
|
254
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
255
|
-
import { AuditLogger } from './logger'
|
|
256
|
-
import { auth } from '@/auth'
|
|
257
|
-
|
|
258
|
-
type Handler = (req: NextRequest, context?: any) => Promise<NextResponse>
|
|
259
|
-
|
|
260
|
-
interface AuditRouteOptions {
|
|
261
|
-
action: string
|
|
262
|
-
entityType: string
|
|
263
|
-
getEntityId?: (req: NextRequest, response: any) => string | undefined
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function auditedRoute(handler: Handler, options: AuditRouteOptions): Handler {
|
|
267
|
-
return async (req: NextRequest, context?: any) => {
|
|
268
|
-
const logger = AuditLogger.fromRequest()
|
|
269
|
-
const session = await auth()
|
|
270
|
-
|
|
271
|
-
if (session?.user?.id) {
|
|
272
|
-
logger.setUser(session.user.id)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const startTime = Date.now()
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const response = await handler(req, context)
|
|
279
|
-
const responseData = await response.clone().json().catch(() => null)
|
|
280
|
-
|
|
281
|
-
await logger.logSuccess({
|
|
282
|
-
action: options.action,
|
|
283
|
-
entityType: options.entityType,
|
|
284
|
-
entityId: options.getEntityId?.(req, responseData),
|
|
285
|
-
metadata: {
|
|
286
|
-
method: req.method,
|
|
287
|
-
path: req.nextUrl.pathname,
|
|
288
|
-
statusCode: response.status,
|
|
289
|
-
duration: Date.now() - startTime
|
|
290
|
-
}
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
return response
|
|
294
|
-
} catch (error) {
|
|
295
|
-
await logger.logFailure(
|
|
296
|
-
{
|
|
297
|
-
action: options.action,
|
|
298
|
-
entityType: options.entityType,
|
|
299
|
-
metadata: {
|
|
300
|
-
method: req.method,
|
|
301
|
-
path: req.nextUrl.pathname,
|
|
302
|
-
duration: Date.now() - startTime
|
|
303
|
-
}
|
|
304
|
-
},
|
|
305
|
-
error as Error
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
throw error
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Usage
|
|
314
|
-
// app/api/users/[id]/route.ts
|
|
315
|
-
export const DELETE = auditedRoute(
|
|
316
|
-
async (req, { params }) => {
|
|
317
|
-
const user = await prisma.user.delete({
|
|
318
|
-
where: { id: params.id }
|
|
319
|
-
})
|
|
320
|
-
return NextResponse.json({ success: true })
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
action: 'user.delete',
|
|
324
|
-
entityType: 'User',
|
|
325
|
-
getEntityId: (req, _) => req.nextUrl.pathname.split('/').pop()
|
|
326
|
-
}
|
|
327
|
-
)
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
## Audit Log Viewer
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
// app/admin/audit/page.tsx
|
|
334
|
-
import { prisma } from '@/lib/db'
|
|
335
|
-
import { formatDistanceToNow } from 'date-fns'
|
|
336
|
-
|
|
337
|
-
interface AuditLogFilters {
|
|
338
|
-
userId?: string
|
|
339
|
-
action?: string
|
|
340
|
-
entityType?: string
|
|
341
|
-
startDate?: Date
|
|
342
|
-
endDate?: Date
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async function getAuditLogs(filters: AuditLogFilters, page = 1, limit = 50) {
|
|
346
|
-
const where: any = {}
|
|
347
|
-
|
|
348
|
-
if (filters.userId) where.userId = filters.userId
|
|
349
|
-
if (filters.action) where.action = { contains: filters.action }
|
|
350
|
-
if (filters.entityType) where.entityType = filters.entityType
|
|
351
|
-
if (filters.startDate || filters.endDate) {
|
|
352
|
-
where.timestamp = {}
|
|
353
|
-
if (filters.startDate) where.timestamp.gte = filters.startDate
|
|
354
|
-
if (filters.endDate) where.timestamp.lte = filters.endDate
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const [logs, total] = await Promise.all([
|
|
358
|
-
prisma.auditLog.findMany({
|
|
359
|
-
where,
|
|
360
|
-
include: { user: { select: { name: true, email: true } } },
|
|
361
|
-
orderBy: { timestamp: 'desc' },
|
|
362
|
-
skip: (page - 1) * limit,
|
|
363
|
-
take: limit
|
|
364
|
-
}),
|
|
365
|
-
prisma.auditLog.count({ where })
|
|
366
|
-
])
|
|
367
|
-
|
|
368
|
-
return { logs, total, pages: Math.ceil(total / limit) }
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export default async function AuditLogPage({
|
|
372
|
-
searchParams
|
|
373
|
-
}: {
|
|
374
|
-
searchParams: { page?: string }
|
|
375
|
-
}) {
|
|
376
|
-
const page = parseInt(searchParams.page ?? '1')
|
|
377
|
-
const { logs, total, pages } = await getAuditLogs({}, page)
|
|
378
|
-
|
|
379
|
-
return (
|
|
380
|
-
<div className="container py-8">
|
|
381
|
-
<h1 className="text-2xl font-bold mb-6">Audit Logs</h1>
|
|
382
|
-
|
|
383
|
-
<table className="w-full">
|
|
384
|
-
<thead>
|
|
385
|
-
<tr className="border-b">
|
|
386
|
-
<th className="text-left py-2">Time</th>
|
|
387
|
-
<th className="text-left py-2">User</th>
|
|
388
|
-
<th className="text-left py-2">Action</th>
|
|
389
|
-
<th className="text-left py-2">Entity</th>
|
|
390
|
-
<th className="text-left py-2">Status</th>
|
|
391
|
-
</tr>
|
|
392
|
-
</thead>
|
|
393
|
-
<tbody>
|
|
394
|
-
{logs.map(log => (
|
|
395
|
-
<tr key={log.id} className="border-b">
|
|
396
|
-
<td className="py-2 text-sm text-gray-600">
|
|
397
|
-
{formatDistanceToNow(log.timestamp, { addSuffix: true })}
|
|
398
|
-
</td>
|
|
399
|
-
<td className="py-2">
|
|
400
|
-
{log.user?.name ?? log.user?.email ?? 'System'}
|
|
401
|
-
</td>
|
|
402
|
-
<td className="py-2">
|
|
403
|
-
<code className="text-sm bg-gray-100 px-1 rounded">
|
|
404
|
-
{log.action}
|
|
405
|
-
</code>
|
|
406
|
-
</td>
|
|
407
|
-
<td className="py-2">
|
|
408
|
-
{log.entityType}
|
|
409
|
-
{log.entityId && (
|
|
410
|
-
<span className="text-gray-500 text-sm ml-1">
|
|
411
|
-
#{log.entityId.slice(0, 8)}
|
|
412
|
-
</span>
|
|
413
|
-
)}
|
|
414
|
-
</td>
|
|
415
|
-
<td className="py-2">
|
|
416
|
-
<span
|
|
417
|
-
className={`px-2 py-0.5 rounded text-xs ${
|
|
418
|
-
log.status === 'success'
|
|
419
|
-
? 'bg-green-100 text-green-800'
|
|
420
|
-
: 'bg-red-100 text-red-800'
|
|
421
|
-
}`}
|
|
422
|
-
>
|
|
423
|
-
{log.status}
|
|
424
|
-
</span>
|
|
425
|
-
</td>
|
|
426
|
-
</tr>
|
|
427
|
-
))}
|
|
428
|
-
</tbody>
|
|
429
|
-
</table>
|
|
430
|
-
|
|
431
|
-
<div className="mt-4 text-sm text-gray-600">
|
|
432
|
-
Showing {logs.length} of {total} logs
|
|
433
|
-
</div>
|
|
434
|
-
</div>
|
|
435
|
-
)
|
|
436
|
-
}
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
## When to Use
|
|
440
|
-
|
|
441
|
-
- Compliance requirements
|
|
442
|
-
- Security monitoring
|
|
443
|
-
- User activity tracking
|
|
444
|
-
- Debugging and forensics
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
# CSRF Protection Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for Cross-Site Request Forgery protection.
|
|
4
|
-
|
|
5
|
-
## Token-Based CSRF Protection
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/csrf.ts
|
|
9
|
-
import { randomBytes, createHmac } from 'crypto'
|
|
10
|
-
import { cookies } from 'next/headers'
|
|
11
|
-
|
|
12
|
-
const CSRF_SECRET = process.env.CSRF_SECRET!
|
|
13
|
-
const CSRF_COOKIE = 'csrf_token'
|
|
14
|
-
const CSRF_HEADER = 'x-csrf-token'
|
|
15
|
-
|
|
16
|
-
export function generateCsrfToken(): string {
|
|
17
|
-
const token = randomBytes(32).toString('hex')
|
|
18
|
-
const signature = createHmac('sha256', CSRF_SECRET)
|
|
19
|
-
.update(token)
|
|
20
|
-
.digest('hex')
|
|
21
|
-
|
|
22
|
-
return `${token}.${signature}`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function validateCsrfToken(token: string): boolean {
|
|
26
|
-
const [value, signature] = token.split('.')
|
|
27
|
-
|
|
28
|
-
if (!value || !signature) return false
|
|
29
|
-
|
|
30
|
-
const expectedSignature = createHmac('sha256', CSRF_SECRET)
|
|
31
|
-
.update(value)
|
|
32
|
-
.digest('hex')
|
|
33
|
-
|
|
34
|
-
return signature === expectedSignature
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function setCsrfCookie() {
|
|
38
|
-
const token = generateCsrfToken()
|
|
39
|
-
const cookieStore = cookies()
|
|
40
|
-
|
|
41
|
-
cookieStore.set(CSRF_COOKIE, token, {
|
|
42
|
-
httpOnly: true,
|
|
43
|
-
secure: process.env.NODE_ENV === 'production',
|
|
44
|
-
sameSite: 'strict',
|
|
45
|
-
path: '/'
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
return token
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function verifyCsrfToken(headerToken: string | null): boolean {
|
|
52
|
-
if (!headerToken) return false
|
|
53
|
-
|
|
54
|
-
const cookieStore = cookies()
|
|
55
|
-
const cookieToken = cookieStore.get(CSRF_COOKIE)?.value
|
|
56
|
-
|
|
57
|
-
if (!cookieToken) return false
|
|
58
|
-
|
|
59
|
-
return headerToken === cookieToken && validateCsrfToken(headerToken)
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## CSRF Middleware
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
// middleware.ts
|
|
67
|
-
import { NextResponse } from 'next/server'
|
|
68
|
-
import type { NextRequest } from 'next/server'
|
|
69
|
-
|
|
70
|
-
const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
|
|
71
|
-
|
|
72
|
-
export function middleware(request: NextRequest) {
|
|
73
|
-
// Skip CSRF check for safe methods
|
|
74
|
-
if (SAFE_METHODS.includes(request.method)) {
|
|
75
|
-
return NextResponse.next()
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Skip for API routes that use other auth (API keys, etc.)
|
|
79
|
-
if (request.nextUrl.pathname.startsWith('/api/webhooks')) {
|
|
80
|
-
return NextResponse.next()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const csrfHeader = request.headers.get('x-csrf-token')
|
|
84
|
-
const csrfCookie = request.cookies.get('csrf_token')?.value
|
|
85
|
-
|
|
86
|
-
if (!csrfHeader || !csrfCookie || csrfHeader !== csrfCookie) {
|
|
87
|
-
return NextResponse.json(
|
|
88
|
-
{ error: 'Invalid CSRF token' },
|
|
89
|
-
{ status: 403 }
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return NextResponse.next()
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export const config = {
|
|
97
|
-
matcher: '/api/:path*'
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## CSRF Token Provider
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
// components/providers/CsrfProvider.tsx
|
|
105
|
-
'use client'
|
|
106
|
-
|
|
107
|
-
import { createContext, useContext, useEffect, useState } from 'react'
|
|
108
|
-
|
|
109
|
-
const CsrfContext = createContext<string | null>(null)
|
|
110
|
-
|
|
111
|
-
export function CsrfProvider({ children }: { children: React.ReactNode }) {
|
|
112
|
-
const [token, setToken] = useState<string | null>(null)
|
|
113
|
-
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
fetch('/api/csrf')
|
|
116
|
-
.then(res => res.json())
|
|
117
|
-
.then(data => setToken(data.token))
|
|
118
|
-
}, [])
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<CsrfContext.Provider value={token}>
|
|
122
|
-
{children}
|
|
123
|
-
</CsrfContext.Provider>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function useCsrfToken() {
|
|
128
|
-
return useContext(CsrfContext)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// API route to get CSRF token
|
|
132
|
-
// app/api/csrf/route.ts
|
|
133
|
-
export async function GET() {
|
|
134
|
-
const token = await setCsrfCookie()
|
|
135
|
-
return Response.json({ token })
|
|
136
|
-
}
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Protected Form
|
|
140
|
-
|
|
141
|
-
```tsx
|
|
142
|
-
// components/forms/ProtectedForm.tsx
|
|
143
|
-
'use client'
|
|
144
|
-
|
|
145
|
-
import { useCsrfToken } from '@/components/providers/CsrfProvider'
|
|
146
|
-
|
|
147
|
-
export function ProtectedForm() {
|
|
148
|
-
const csrfToken = useCsrfToken()
|
|
149
|
-
|
|
150
|
-
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
151
|
-
e.preventDefault()
|
|
152
|
-
const formData = new FormData(e.currentTarget)
|
|
153
|
-
|
|
154
|
-
const response = await fetch('/api/submit', {
|
|
155
|
-
method: 'POST',
|
|
156
|
-
headers: {
|
|
157
|
-
'Content-Type': 'application/json',
|
|
158
|
-
'X-CSRF-Token': csrfToken ?? ''
|
|
159
|
-
},
|
|
160
|
-
body: JSON.stringify(Object.fromEntries(formData))
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
// Handle response...
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
<form onSubmit={handleSubmit}>
|
|
168
|
-
<input type="hidden" name="csrf_token" value={csrfToken ?? ''} />
|
|
169
|
-
{/* Form fields */}
|
|
170
|
-
<button type="submit">Submit</button>
|
|
171
|
-
</form>
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## Double Submit Cookie
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
// lib/csrf-double-submit.ts
|
|
180
|
-
import { cookies } from 'next/headers'
|
|
181
|
-
import { randomBytes } from 'crypto'
|
|
182
|
-
|
|
183
|
-
export function getOrCreateCsrfToken(): string {
|
|
184
|
-
const cookieStore = cookies()
|
|
185
|
-
let token = cookieStore.get('csrf')?.value
|
|
186
|
-
|
|
187
|
-
if (!token) {
|
|
188
|
-
token = randomBytes(32).toString('hex')
|
|
189
|
-
cookieStore.set('csrf', token, {
|
|
190
|
-
httpOnly: false, // Must be readable by JS
|
|
191
|
-
secure: process.env.NODE_ENV === 'production',
|
|
192
|
-
sameSite: 'strict'
|
|
193
|
-
})
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return token
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Client-side: read cookie and send in header
|
|
200
|
-
export function getCsrfTokenFromCookie(): string | null {
|
|
201
|
-
const match = document.cookie.match(/csrf=([^;]+)/)
|
|
202
|
-
return match ? match[1] : null
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## SameSite Cookie Protection
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
// lib/session.ts
|
|
210
|
-
// Modern approach: rely on SameSite cookies
|
|
211
|
-
|
|
212
|
-
export function setSessionCookie(sessionId: string) {
|
|
213
|
-
const cookieStore = cookies()
|
|
214
|
-
|
|
215
|
-
cookieStore.set('session', sessionId, {
|
|
216
|
-
httpOnly: true,
|
|
217
|
-
secure: true,
|
|
218
|
-
sameSite: 'lax', // or 'strict' for more protection
|
|
219
|
-
path: '/',
|
|
220
|
-
maxAge: 60 * 60 * 24 * 7 // 1 week
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// SameSite=Lax: Cookie sent with top-level navigations and GET from third-party
|
|
225
|
-
// SameSite=Strict: Cookie only sent in first-party context
|
|
226
|
-
// Combined with secure: true, this prevents most CSRF attacks
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
## When to Use
|
|
230
|
-
|
|
231
|
-
- Form submissions
|
|
232
|
-
- State-changing operations
|
|
233
|
-
- API mutations
|
|
234
|
-
- Authenticated requests
|