@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,367 +0,0 @@
|
|
|
1
|
-
# Session Management Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for managing user sessions.
|
|
4
|
-
|
|
5
|
-
## JWT Session Configuration
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// auth.config.ts
|
|
9
|
-
import { NextAuthConfig } from 'next-auth'
|
|
10
|
-
|
|
11
|
-
export const authConfig: NextAuthConfig = {
|
|
12
|
-
session: {
|
|
13
|
-
strategy: 'jwt',
|
|
14
|
-
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
15
|
-
updateAge: 24 * 60 * 60 // 24 hours
|
|
16
|
-
},
|
|
17
|
-
callbacks: {
|
|
18
|
-
jwt({ token, user, trigger, session }) {
|
|
19
|
-
// Initial sign in
|
|
20
|
-
if (user) {
|
|
21
|
-
token.id = user.id
|
|
22
|
-
token.role = user.role
|
|
23
|
-
token.organizationId = user.organizationId
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Update session when trigger is 'update'
|
|
27
|
-
if (trigger === 'update' && session) {
|
|
28
|
-
token.name = session.name
|
|
29
|
-
token.organizationId = session.organizationId
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return token
|
|
33
|
-
},
|
|
34
|
-
session({ session, token }) {
|
|
35
|
-
session.user.id = token.id as string
|
|
36
|
-
session.user.role = token.role as string
|
|
37
|
-
session.user.organizationId = token.organizationId as string
|
|
38
|
-
|
|
39
|
-
return session
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Database Sessions
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
// auth.ts
|
|
49
|
-
import NextAuth from 'next-auth'
|
|
50
|
-
import { PrismaAdapter } from '@auth/prisma-adapter'
|
|
51
|
-
import { prisma } from '@/lib/db'
|
|
52
|
-
|
|
53
|
-
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
54
|
-
adapter: PrismaAdapter(prisma),
|
|
55
|
-
session: {
|
|
56
|
-
strategy: 'database',
|
|
57
|
-
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
58
|
-
updateAge: 24 * 60 * 60 // Update session every 24 hours
|
|
59
|
-
},
|
|
60
|
-
callbacks: {
|
|
61
|
-
session({ session, user }) {
|
|
62
|
-
session.user.id = user.id
|
|
63
|
-
session.user.role = user.role
|
|
64
|
-
return session
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
providers: [
|
|
68
|
-
// ... providers
|
|
69
|
-
]
|
|
70
|
-
})
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Session Refresh
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// lib/auth/session.ts
|
|
77
|
-
import { auth, signIn } from '@/auth'
|
|
78
|
-
import { redirect } from 'next/navigation'
|
|
79
|
-
|
|
80
|
-
export async function getSession() {
|
|
81
|
-
const session = await auth()
|
|
82
|
-
|
|
83
|
-
if (!session) {
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check if session is about to expire
|
|
88
|
-
const expiresAt = new Date(session.expires)
|
|
89
|
-
const now = new Date()
|
|
90
|
-
const daysUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
|
91
|
-
|
|
92
|
-
if (daysUntilExpiry < 7) {
|
|
93
|
-
// Session will expire soon, refresh it
|
|
94
|
-
// This triggers the updateAge callback
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return session
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export async function requireAuth() {
|
|
101
|
-
const session = await getSession()
|
|
102
|
-
|
|
103
|
-
if (!session) {
|
|
104
|
-
redirect('/login')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return session
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function requireRole(requiredRoles: string[]) {
|
|
111
|
-
const session = await requireAuth()
|
|
112
|
-
|
|
113
|
-
if (!requiredRoles.includes(session.user.role)) {
|
|
114
|
-
redirect('/unauthorized')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return session
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## Session Update
|
|
122
|
-
|
|
123
|
-
```tsx
|
|
124
|
-
// components/UpdateSessionForm.tsx
|
|
125
|
-
'use client'
|
|
126
|
-
|
|
127
|
-
import { useSession } from 'next-auth/react'
|
|
128
|
-
import { useState } from 'react'
|
|
129
|
-
|
|
130
|
-
export function UpdateSessionForm() {
|
|
131
|
-
const { data: session, update } = useSession()
|
|
132
|
-
const [name, setName] = useState(session?.user?.name ?? '')
|
|
133
|
-
|
|
134
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
135
|
-
e.preventDefault()
|
|
136
|
-
|
|
137
|
-
// Update user in database
|
|
138
|
-
await fetch('/api/user/profile', {
|
|
139
|
-
method: 'PATCH',
|
|
140
|
-
body: JSON.stringify({ name })
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
// Update session
|
|
144
|
-
await update({ name })
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
<form onSubmit={handleSubmit}>
|
|
149
|
-
<input
|
|
150
|
-
type="text"
|
|
151
|
-
value={name}
|
|
152
|
-
onChange={e => setName(e.target.value)}
|
|
153
|
-
/>
|
|
154
|
-
<button type="submit">Update</button>
|
|
155
|
-
</form>
|
|
156
|
-
)
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Active Sessions Management
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
// prisma/schema.prisma
|
|
164
|
-
model Session {
|
|
165
|
-
id String @id @default(cuid())
|
|
166
|
-
sessionToken String @unique
|
|
167
|
-
userId String
|
|
168
|
-
expires DateTime
|
|
169
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
170
|
-
|
|
171
|
-
// Additional fields for session management
|
|
172
|
-
userAgent String?
|
|
173
|
-
ipAddress String?
|
|
174
|
-
lastActive DateTime @default(now())
|
|
175
|
-
createdAt DateTime @default(now())
|
|
176
|
-
|
|
177
|
-
@@index([userId])
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
// lib/auth/sessions.ts
|
|
183
|
-
import { prisma } from '@/lib/db'
|
|
184
|
-
import { headers } from 'next/headers'
|
|
185
|
-
|
|
186
|
-
export async function getUserSessions(userId: string) {
|
|
187
|
-
return prisma.session.findMany({
|
|
188
|
-
where: { userId },
|
|
189
|
-
orderBy: { lastActive: 'desc' },
|
|
190
|
-
select: {
|
|
191
|
-
id: true,
|
|
192
|
-
userAgent: true,
|
|
193
|
-
ipAddress: true,
|
|
194
|
-
lastActive: true,
|
|
195
|
-
createdAt: true,
|
|
196
|
-
expires: true
|
|
197
|
-
}
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export async function revokeSession(sessionId: string, userId: string) {
|
|
202
|
-
return prisma.session.deleteMany({
|
|
203
|
-
where: {
|
|
204
|
-
id: sessionId,
|
|
205
|
-
userId // Ensure user owns the session
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export async function revokeAllOtherSessions(
|
|
211
|
-
currentSessionToken: string,
|
|
212
|
-
userId: string
|
|
213
|
-
) {
|
|
214
|
-
return prisma.session.deleteMany({
|
|
215
|
-
where: {
|
|
216
|
-
userId,
|
|
217
|
-
NOT: { sessionToken: currentSessionToken }
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export async function updateSessionActivity(sessionToken: string) {
|
|
223
|
-
const headersList = headers()
|
|
224
|
-
|
|
225
|
-
await prisma.session.update({
|
|
226
|
-
where: { sessionToken },
|
|
227
|
-
data: {
|
|
228
|
-
lastActive: new Date(),
|
|
229
|
-
userAgent: headersList.get('user-agent'),
|
|
230
|
-
ipAddress:
|
|
231
|
-
headersList.get('x-forwarded-for') ??
|
|
232
|
-
headersList.get('x-real-ip')
|
|
233
|
-
}
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## Session List UI
|
|
239
|
-
|
|
240
|
-
```tsx
|
|
241
|
-
// app/settings/sessions/page.tsx
|
|
242
|
-
import { auth } from '@/auth'
|
|
243
|
-
import { getUserSessions, revokeSession } from '@/lib/auth/sessions'
|
|
244
|
-
import { formatDistanceToNow } from 'date-fns'
|
|
245
|
-
import { UAParser } from 'ua-parser-js'
|
|
246
|
-
|
|
247
|
-
export default async function SessionsPage() {
|
|
248
|
-
const session = await auth()
|
|
249
|
-
if (!session) return null
|
|
250
|
-
|
|
251
|
-
const sessions = await getUserSessions(session.user.id)
|
|
252
|
-
|
|
253
|
-
return (
|
|
254
|
-
<div className="space-y-6">
|
|
255
|
-
<h2 className="text-xl font-semibold">Active Sessions</h2>
|
|
256
|
-
|
|
257
|
-
<div className="space-y-4">
|
|
258
|
-
{sessions.map(s => {
|
|
259
|
-
const ua = new UAParser(s.userAgent ?? '').getResult()
|
|
260
|
-
const isCurrent = s.id === session.sessionId
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
<div
|
|
264
|
-
key={s.id}
|
|
265
|
-
className="flex items-center justify-between rounded-lg border p-4"
|
|
266
|
-
>
|
|
267
|
-
<div>
|
|
268
|
-
<div className="flex items-center gap-2">
|
|
269
|
-
<span className="font-medium">
|
|
270
|
-
{ua.browser.name} on {ua.os.name}
|
|
271
|
-
</span>
|
|
272
|
-
{isCurrent && (
|
|
273
|
-
<span className="rounded bg-green-100 px-2 py-0.5 text-xs text-green-800">
|
|
274
|
-
Current
|
|
275
|
-
</span>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
<div className="text-sm text-gray-500">
|
|
279
|
-
<span>{s.ipAddress}</span>
|
|
280
|
-
<span className="mx-2">•</span>
|
|
281
|
-
<span>
|
|
282
|
-
Last active {formatDistanceToNow(s.lastActive, { addSuffix: true })}
|
|
283
|
-
</span>
|
|
284
|
-
</div>
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
{!isCurrent && (
|
|
288
|
-
<form action={async () => {
|
|
289
|
-
'use server'
|
|
290
|
-
await revokeSession(s.id, session.user.id)
|
|
291
|
-
}}>
|
|
292
|
-
<button
|
|
293
|
-
type="submit"
|
|
294
|
-
className="text-sm text-red-600 hover:underline"
|
|
295
|
-
>
|
|
296
|
-
Revoke
|
|
297
|
-
</button>
|
|
298
|
-
</form>
|
|
299
|
-
)}
|
|
300
|
-
</div>
|
|
301
|
-
)
|
|
302
|
-
})}
|
|
303
|
-
</div>
|
|
304
|
-
|
|
305
|
-
<form action={async () => {
|
|
306
|
-
'use server'
|
|
307
|
-
await revokeAllOtherSessions(session.sessionToken!, session.user.id)
|
|
308
|
-
}}>
|
|
309
|
-
<button
|
|
310
|
-
type="submit"
|
|
311
|
-
className="text-sm text-red-600 hover:underline"
|
|
312
|
-
>
|
|
313
|
-
Sign out all other sessions
|
|
314
|
-
</button>
|
|
315
|
-
</form>
|
|
316
|
-
</div>
|
|
317
|
-
)
|
|
318
|
-
}
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
## Session Middleware
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
// middleware.ts
|
|
325
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
326
|
-
import { auth } from '@/auth'
|
|
327
|
-
|
|
328
|
-
export async function middleware(request: NextRequest) {
|
|
329
|
-
const session = await auth()
|
|
330
|
-
|
|
331
|
-
// Check if session exists for protected routes
|
|
332
|
-
if (request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
333
|
-
if (!session) {
|
|
334
|
-
return NextResponse.redirect(new URL('/login', request.url))
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Check session expiry
|
|
338
|
-
if (new Date(session.expires) < new Date()) {
|
|
339
|
-
const response = NextResponse.redirect(new URL('/login', request.url))
|
|
340
|
-
// Clear invalid session cookie
|
|
341
|
-
response.cookies.delete('next-auth.session-token')
|
|
342
|
-
return response
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Add session info to headers for logging
|
|
347
|
-
const requestHeaders = new Headers(request.headers)
|
|
348
|
-
if (session?.user) {
|
|
349
|
-
requestHeaders.set('x-user-id', session.user.id)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return NextResponse.next({
|
|
353
|
-
request: { headers: requestHeaders }
|
|
354
|
-
})
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export const config = {
|
|
358
|
-
matcher: ['/dashboard/:path*', '/api/:path*']
|
|
359
|
-
}
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
## When to Use
|
|
363
|
-
|
|
364
|
-
- User authentication
|
|
365
|
-
- Multi-device management
|
|
366
|
-
- Security auditing
|
|
367
|
-
- Session invalidation
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# Session Management Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for managing user sessions with JWT and cookies.
|
|
4
|
-
|
|
5
|
-
## JWT Session Creation
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/session.ts
|
|
9
|
-
import { SignJWT, jwtVerify } from 'jose'
|
|
10
|
-
import { cookies } from 'next/headers'
|
|
11
|
-
|
|
12
|
-
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
|
|
13
|
-
const COOKIE_NAME = 'session'
|
|
14
|
-
|
|
15
|
-
export async function createSession(userId: string) {
|
|
16
|
-
const token = await new SignJWT({ userId })
|
|
17
|
-
.setProtectedHeader({ alg: 'HS256' })
|
|
18
|
-
.setIssuedAt()
|
|
19
|
-
.setExpirationTime('7d')
|
|
20
|
-
.sign(secret)
|
|
21
|
-
|
|
22
|
-
cookies().set(COOKIE_NAME, token, {
|
|
23
|
-
httpOnly: true,
|
|
24
|
-
secure: process.env.NODE_ENV === 'production',
|
|
25
|
-
sameSite: 'lax',
|
|
26
|
-
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
27
|
-
path: '/'
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
return token
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Session Verification
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// lib/session.ts
|
|
38
|
-
export async function getSession() {
|
|
39
|
-
const token = cookies().get(COOKIE_NAME)?.value
|
|
40
|
-
|
|
41
|
-
if (!token) return null
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const { payload } = await jwtVerify(token, secret)
|
|
45
|
-
return payload as { userId: string }
|
|
46
|
-
} catch {
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function requireSession() {
|
|
52
|
-
const session = await getSession()
|
|
53
|
-
|
|
54
|
-
if (!session) {
|
|
55
|
-
throw new Error('Unauthorized')
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return session
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Session Refresh
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
// lib/session.ts
|
|
66
|
-
export async function refreshSession() {
|
|
67
|
-
const session = await getSession()
|
|
68
|
-
|
|
69
|
-
if (!session) return null
|
|
70
|
-
|
|
71
|
-
// Create new token with extended expiry
|
|
72
|
-
return createSession(session.userId)
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Session Destruction
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
// lib/session.ts
|
|
80
|
-
export async function destroySession() {
|
|
81
|
-
cookies().delete(COOKIE_NAME)
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Session Middleware
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
// middleware.ts
|
|
89
|
-
import { NextResponse } from 'next/server'
|
|
90
|
-
import type { NextRequest } from 'next/server'
|
|
91
|
-
import { jwtVerify } from 'jose'
|
|
92
|
-
|
|
93
|
-
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
|
|
94
|
-
|
|
95
|
-
export async function middleware(request: NextRequest) {
|
|
96
|
-
const token = request.cookies.get('session')?.value
|
|
97
|
-
|
|
98
|
-
if (!token) {
|
|
99
|
-
return NextResponse.redirect(new URL('/login', request.url))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
await jwtVerify(token, secret)
|
|
104
|
-
return NextResponse.next()
|
|
105
|
-
} catch {
|
|
106
|
-
return NextResponse.redirect(new URL('/login', request.url))
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export const config = {
|
|
111
|
-
matcher: ['/dashboard/:path*', '/settings/:path*']
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## When to Use
|
|
116
|
-
|
|
117
|
-
- Custom authentication systems
|
|
118
|
-
- Stateless session management
|
|
119
|
-
- Edge-compatible auth
|
|
120
|
-
- API authentication
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
# Audit Log Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for tracking data changes with audit logs.
|
|
4
|
-
|
|
5
|
-
## Audit Log Schema
|
|
6
|
-
|
|
7
|
-
```prisma
|
|
8
|
-
// schema.prisma
|
|
9
|
-
model AuditLog {
|
|
10
|
-
id String @id @default(cuid())
|
|
11
|
-
entityType String // "User", "Post", etc.
|
|
12
|
-
entityId String
|
|
13
|
-
action String // "CREATE", "UPDATE", "DELETE"
|
|
14
|
-
changes Json? // What changed
|
|
15
|
-
userId String? // Who made the change
|
|
16
|
-
user User? @relation(fields: [userId], references: [id])
|
|
17
|
-
ipAddress String?
|
|
18
|
-
userAgent String?
|
|
19
|
-
createdAt DateTime @default(now())
|
|
20
|
-
|
|
21
|
-
@@index([entityType, entityId])
|
|
22
|
-
@@index([userId])
|
|
23
|
-
@@index([createdAt])
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Audit Logger
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
// lib/audit.ts
|
|
31
|
-
import { prisma } from '@/lib/db'
|
|
32
|
-
|
|
33
|
-
interface AuditContext {
|
|
34
|
-
userId?: string
|
|
35
|
-
ipAddress?: string
|
|
36
|
-
userAgent?: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function createAuditLog(
|
|
40
|
-
entityType: string,
|
|
41
|
-
entityId: string,
|
|
42
|
-
action: 'CREATE' | 'UPDATE' | 'DELETE',
|
|
43
|
-
changes?: object,
|
|
44
|
-
context?: AuditContext
|
|
45
|
-
) {
|
|
46
|
-
return prisma.auditLog.create({
|
|
47
|
-
data: {
|
|
48
|
-
entityType,
|
|
49
|
-
entityId,
|
|
50
|
-
action,
|
|
51
|
-
changes: changes ? JSON.parse(JSON.stringify(changes)) : null,
|
|
52
|
-
userId: context?.userId,
|
|
53
|
-
ipAddress: context?.ipAddress,
|
|
54
|
-
userAgent: context?.userAgent
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Change Detection
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
// lib/audit.ts
|
|
64
|
-
export function detectChanges(
|
|
65
|
-
before: Record<string, any>,
|
|
66
|
-
after: Record<string, any>
|
|
67
|
-
) {
|
|
68
|
-
const changes: Record<string, { from: any; to: any }> = {}
|
|
69
|
-
|
|
70
|
-
for (const key of Object.keys(after)) {
|
|
71
|
-
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
|
|
72
|
-
changes[key] = {
|
|
73
|
-
from: before[key],
|
|
74
|
-
to: after[key]
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return Object.keys(changes).length > 0 ? changes : null
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Audited Update
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
// lib/users.ts
|
|
87
|
-
import { prisma } from '@/lib/db'
|
|
88
|
-
import { createAuditLog, detectChanges } from '@/lib/audit'
|
|
89
|
-
|
|
90
|
-
export async function updateUser(
|
|
91
|
-
id: string,
|
|
92
|
-
data: Partial<User>,
|
|
93
|
-
context: AuditContext
|
|
94
|
-
) {
|
|
95
|
-
// Get current state
|
|
96
|
-
const before = await prisma.user.findUnique({ where: { id } })
|
|
97
|
-
if (!before) throw new Error('User not found')
|
|
98
|
-
|
|
99
|
-
// Update
|
|
100
|
-
const after = await prisma.user.update({
|
|
101
|
-
where: { id },
|
|
102
|
-
data
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
// Log changes
|
|
106
|
-
const changes = detectChanges(before, after)
|
|
107
|
-
if (changes) {
|
|
108
|
-
await createAuditLog('User', id, 'UPDATE', changes, context)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return after
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Prisma Middleware Approach
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
// lib/db.ts
|
|
119
|
-
import { PrismaClient } from '@prisma/client'
|
|
120
|
-
|
|
121
|
-
const prisma = new PrismaClient()
|
|
122
|
-
|
|
123
|
-
// Global audit middleware
|
|
124
|
-
prisma.$use(async (params, next) => {
|
|
125
|
-
const result = await next(params)
|
|
126
|
-
|
|
127
|
-
// Skip audit log model itself
|
|
128
|
-
if (params.model === 'AuditLog') return result
|
|
129
|
-
|
|
130
|
-
// Log mutations
|
|
131
|
-
if (['create', 'update', 'delete'].includes(params.action)) {
|
|
132
|
-
await prisma.auditLog.create({
|
|
133
|
-
data: {
|
|
134
|
-
entityType: params.model!,
|
|
135
|
-
entityId: result?.id || params.args.where?.id,
|
|
136
|
-
action: params.action.toUpperCase(),
|
|
137
|
-
changes: params.args.data
|
|
138
|
-
}
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return result
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
export { prisma }
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Query Audit History
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
// lib/audit.ts
|
|
152
|
-
export async function getEntityHistory(
|
|
153
|
-
entityType: string,
|
|
154
|
-
entityId: string
|
|
155
|
-
) {
|
|
156
|
-
return prisma.auditLog.findMany({
|
|
157
|
-
where: { entityType, entityId },
|
|
158
|
-
orderBy: { createdAt: 'desc' },
|
|
159
|
-
include: { user: { select: { name: true, email: true } } }
|
|
160
|
-
})
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function getUserActivity(userId: string, limit = 50) {
|
|
164
|
-
return prisma.auditLog.findMany({
|
|
165
|
-
where: { userId },
|
|
166
|
-
orderBy: { createdAt: 'desc' },
|
|
167
|
-
take: limit
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## When to Use
|
|
173
|
-
|
|
174
|
-
- Compliance requirements (HIPAA, SOX)
|
|
175
|
-
- Debug and troubleshooting
|
|
176
|
-
- User activity tracking
|
|
177
|
-
- Undo functionality
|