@mars-stack/cli 0.2.0 → 1.0.2

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.
Files changed (175) hide show
  1. package/dist/index.js +137 -12
  2. package/dist/index.js.map +1 -1
  3. package/package.json +4 -3
  4. package/template/.cursor/rules/composition-patterns.mdc +186 -0
  5. package/template/.cursor/rules/data-access.mdc +29 -0
  6. package/template/.cursor/rules/project-structure.mdc +34 -0
  7. package/template/.cursor/rules/security.mdc +25 -0
  8. package/template/.cursor/rules/testing.mdc +24 -0
  9. package/template/.cursor/rules/ui-conventions.mdc +29 -0
  10. package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
  11. package/template/.cursor/skills/add-audit-log/SKILL.md +375 -0
  12. package/template/.cursor/skills/add-blog/SKILL.md +447 -0
  13. package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
  14. package/template/.cursor/skills/add-component/SKILL.md +158 -0
  15. package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
  16. package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
  17. package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
  18. package/template/.cursor/skills/add-feature/SKILL.md +174 -0
  19. package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
  20. package/template/.cursor/skills/add-page/SKILL.md +151 -0
  21. package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
  22. package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
  23. package/template/.cursor/skills/add-role/SKILL.md +156 -0
  24. package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
  25. package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
  26. package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
  27. package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
  28. package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
  29. package/template/.cursor/skills/build-form/SKILL.md +231 -0
  30. package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
  31. package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
  32. package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
  33. package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
  34. package/template/.cursor/skills/configure-email/SKILL.md +170 -0
  35. package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
  36. package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
  37. package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
  38. package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
  39. package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
  40. package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
  41. package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
  42. package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
  43. package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
  44. package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
  45. package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
  46. package/template/.cursor/skills/configure-search/SKILL.md +581 -0
  47. package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
  48. package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
  49. package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
  50. package/template/.cursor/skills/create-seed/SKILL.md +191 -0
  51. package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
  52. package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
  53. package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
  54. package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
  55. package/template/.cursor/skills/setup-project/SKILL.md +104 -0
  56. package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
  57. package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
  58. package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
  59. package/template/AGENTS.md +104 -0
  60. package/template/ARCHITECTURE.md +102 -0
  61. package/template/docs/QUALITY_SCORE.md +20 -0
  62. package/template/docs/design-docs/conversation-as-system-record.md +70 -0
  63. package/template/docs/design-docs/core-beliefs.md +43 -0
  64. package/template/docs/design-docs/index.md +8 -0
  65. package/template/docs/exec-plans/active/.gitkeep +0 -0
  66. package/template/docs/exec-plans/completed/.gitkeep +0 -0
  67. package/template/docs/exec-plans/tech-debt.md +7 -0
  68. package/template/docs/generated/.gitkeep +0 -0
  69. package/template/docs/product-specs/index.md +7 -0
  70. package/template/docs/references/index.md +18 -0
  71. package/template/e2e/api.spec.ts +20 -0
  72. package/template/e2e/auth.spec.ts +24 -0
  73. package/template/e2e/public.spec.ts +25 -0
  74. package/template/eslint.config.mjs +24 -0
  75. package/template/next-env.d.ts +6 -0
  76. package/template/next.config.ts +45 -0
  77. package/template/package.json +80 -0
  78. package/template/playwright.config.ts +31 -0
  79. package/template/postcss.config.mjs +8 -0
  80. package/template/prisma/generated/prisma/browser.ts +49 -0
  81. package/template/prisma/generated/prisma/client.ts +73 -0
  82. package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
  83. package/template/prisma/generated/prisma/enums.ts +15 -0
  84. package/template/prisma/generated/prisma/internal/class.ts +254 -0
  85. package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
  86. package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
  87. package/template/prisma/generated/prisma/models/Account.ts +1543 -0
  88. package/template/prisma/generated/prisma/models/File.ts +1529 -0
  89. package/template/prisma/generated/prisma/models/Session.ts +1415 -0
  90. package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
  91. package/template/prisma/generated/prisma/models/User.ts +2235 -0
  92. package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
  93. package/template/prisma/generated/prisma/models.ts +17 -0
  94. package/template/prisma/schema/auth.prisma +69 -0
  95. package/template/prisma/schema/base.prisma +8 -0
  96. package/template/prisma/schema/file.prisma +15 -0
  97. package/template/prisma/schema/subscription.prisma +17 -0
  98. package/template/prisma.config.ts +13 -0
  99. package/template/scripts/check-architecture.ts +221 -0
  100. package/template/scripts/check-doc-freshness.ts +242 -0
  101. package/template/scripts/ensure-db.mjs +291 -0
  102. package/template/scripts/generate-docs.ts +143 -0
  103. package/template/scripts/generate-env-example.ts +89 -0
  104. package/template/scripts/seed.ts +56 -0
  105. package/template/scripts/update-quality-score.ts +263 -0
  106. package/template/src/__tests__/architecture.test.ts +114 -0
  107. package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
  108. package/template/src/app/(auth)/layout.tsx +11 -0
  109. package/template/src/app/(auth)/register/page.tsx +162 -0
  110. package/template/src/app/(auth)/reset-password/page.tsx +109 -0
  111. package/template/src/app/(auth)/sign-in/page.tsx +122 -0
  112. package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
  113. package/template/src/app/(auth)/verify/page.tsx +56 -0
  114. package/template/src/app/(protected)/admin/page.tsx +108 -0
  115. package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
  116. package/template/src/app/(protected)/dashboard/page.tsx +22 -0
  117. package/template/src/app/(protected)/layout.tsx +262 -0
  118. package/template/src/app/(protected)/settings/page.tsx +370 -0
  119. package/template/src/app/api/auth/forgot/route.ts +63 -0
  120. package/template/src/app/api/auth/login/route.ts +121 -0
  121. package/template/src/app/api/auth/logout/route.ts +19 -0
  122. package/template/src/app/api/auth/me/route.ts +30 -0
  123. package/template/src/app/api/auth/reset/route.ts +45 -0
  124. package/template/src/app/api/auth/signup/route.ts +85 -0
  125. package/template/src/app/api/auth/verify/route.ts +46 -0
  126. package/template/src/app/api/csrf/route.ts +12 -0
  127. package/template/src/app/api/health/route.ts +10 -0
  128. package/template/src/app/api/protected/admin/users/route.ts +24 -0
  129. package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
  130. package/template/src/app/api/protected/billing/portal/route.ts +39 -0
  131. package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
  132. package/template/src/app/api/protected/files/upload/route.ts +64 -0
  133. package/template/src/app/api/protected/user/password/route.ts +63 -0
  134. package/template/src/app/api/protected/user/profile/route.ts +35 -0
  135. package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
  136. package/template/src/app/api/protected/user/sessions/route.ts +22 -0
  137. package/template/src/app/api/readiness/route.ts +15 -0
  138. package/template/src/app/api/webhooks/stripe/route.ts +166 -0
  139. package/template/src/app/error.tsx +33 -0
  140. package/template/src/app/layout.tsx +29 -0
  141. package/template/src/app/not-found.tsx +20 -0
  142. package/template/src/app/page.tsx +136 -0
  143. package/template/src/app/privacy/page.tsx +178 -0
  144. package/template/src/app/providers.tsx +8 -0
  145. package/template/src/app/terms/page.tsx +139 -0
  146. package/template/src/config/app.config.ts +70 -0
  147. package/template/src/config/routes.ts +17 -0
  148. package/template/src/features/admin/index.ts +11 -0
  149. package/template/src/features/admin/permissions.ts +64 -0
  150. package/template/src/features/auth/context/AuthContext.tsx +96 -0
  151. package/template/src/features/auth/context/index.ts +2 -0
  152. package/template/src/features/auth/index.ts +3 -0
  153. package/template/src/features/auth/server/consent.ts +66 -0
  154. package/template/src/features/auth/server/session-revocation.ts +20 -0
  155. package/template/src/features/auth/server/sessions.ts +66 -0
  156. package/template/src/features/auth/server/user.ts +166 -0
  157. package/template/src/features/auth/types.ts +19 -0
  158. package/template/src/features/auth/validators.ts +29 -0
  159. package/template/src/features/billing/server/index.ts +66 -0
  160. package/template/src/features/billing/types.ts +43 -0
  161. package/template/src/features/uploads/server/index.ts +49 -0
  162. package/template/src/features/uploads/types.ts +26 -0
  163. package/template/src/lib/core/email/templates/base-layout.ts +122 -0
  164. package/template/src/lib/core/email/templates/index.ts +4 -0
  165. package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
  166. package/template/src/lib/core/email/templates/verification-email.ts +41 -0
  167. package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
  168. package/template/src/lib/mars.ts +56 -0
  169. package/template/src/lib/prisma.ts +19 -0
  170. package/template/src/proxy.ts +92 -0
  171. package/template/src/styles/brand.css +15 -0
  172. package/template/src/styles/globals.css +7 -0
  173. package/template/tsconfig.json +59 -0
  174. package/template/vitest.config.ts +41 -0
  175. package/template/vitest.setup.ts +24 -0
@@ -0,0 +1,375 @@
1
+ # Skill: Add Audit Log
2
+
3
+ Add user action tracking with a Prisma `AuditLog` model, server-side logging helper, admin view, and retention policy.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to add audit logging, activity tracking, compliance logging, user action history, or admin activity monitoring.
8
+
9
+ ## Prerequisites
10
+
11
+ - Read `src/lib/mars.ts` to check available auth wrappers.
12
+ - Read `prisma/schema/` to see existing models.
13
+
14
+ ## Step 1: Prisma Schema
15
+
16
+ ```prisma
17
+ // prisma/schema/audit-log.prisma
18
+ model AuditLog {
19
+ id String @id @default(cuid())
20
+ userId String?
21
+ action String
22
+ resourceType String
23
+ resourceId String?
24
+ metadata Json?
25
+ ipAddress String?
26
+ userAgent String?
27
+ timestamp DateTime @default(now())
28
+
29
+ user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
30
+
31
+ @@index([userId])
32
+ @@index([action])
33
+ @@index([resourceType])
34
+ @@index([resourceType, resourceId])
35
+ @@index([timestamp])
36
+ }
37
+ ```
38
+
39
+ Update the User model in `prisma/schema/auth.prisma` to add the relation:
40
+
41
+ ```prisma
42
+ model User {
43
+ // ... existing fields
44
+ auditLogs AuditLog[]
45
+ }
46
+ ```
47
+
48
+ Run `yarn db:push` to sync.
49
+
50
+ ## Step 2: Audit Log Types
51
+
52
+ ```typescript
53
+ // src/features/audit-log/types.ts
54
+ export interface AuditEventInput {
55
+ userId?: string;
56
+ action: AuditAction;
57
+ resourceType: string;
58
+ resourceId?: string;
59
+ metadata?: Record<string, unknown>;
60
+ ipAddress?: string;
61
+ userAgent?: string;
62
+ }
63
+
64
+ export type AuditAction =
65
+ | 'create'
66
+ | 'read'
67
+ | 'update'
68
+ | 'delete'
69
+ | 'login'
70
+ | 'logout'
71
+ | 'login_failed'
72
+ | 'password_change'
73
+ | 'password_reset'
74
+ | 'email_verify'
75
+ | 'role_change'
76
+ | 'export'
77
+ | 'invite'
78
+ | 'settings_change';
79
+
80
+ export interface AuditLogQuery {
81
+ userId?: string;
82
+ action?: AuditAction;
83
+ resourceType?: string;
84
+ resourceId?: string;
85
+ startDate?: Date;
86
+ endDate?: Date;
87
+ limit?: number;
88
+ cursor?: string;
89
+ }
90
+ ```
91
+
92
+ ## Step 3: Server-Side Audit Logger
93
+
94
+ ```typescript
95
+ // src/features/audit-log/server/index.ts
96
+ import 'server-only';
97
+
98
+ import { prisma } from '@/lib/prisma';
99
+ import { apiLogger } from '@/lib/mars';
100
+ import type { AuditEventInput, AuditLogQuery } from '../types';
101
+
102
+ export async function logAuditEvent(event: AuditEventInput): Promise<void> {
103
+ try {
104
+ await prisma.auditLog.create({
105
+ data: {
106
+ userId: event.userId,
107
+ action: event.action,
108
+ resourceType: event.resourceType,
109
+ resourceId: event.resourceId,
110
+ metadata: event.metadata ?? undefined,
111
+ ipAddress: event.ipAddress,
112
+ userAgent: event.userAgent,
113
+ },
114
+ });
115
+ } catch (error) {
116
+ apiLogger.error({ error, event: event.action }, 'Failed to write audit log');
117
+ }
118
+ }
119
+
120
+ export function logAuditEventFromRequest(
121
+ request: Request,
122
+ event: Omit<AuditEventInput, 'ipAddress' | 'userAgent'>,
123
+ ): Promise<void> {
124
+ return logAuditEvent({
125
+ ...event,
126
+ ipAddress: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
127
+ ?? request.headers.get('x-real-ip')
128
+ ?? undefined,
129
+ userAgent: request.headers.get('user-agent') ?? undefined,
130
+ });
131
+ }
132
+
133
+ export async function queryAuditLogs(query: AuditLogQuery) {
134
+ const where: Record<string, unknown> = {};
135
+
136
+ if (query.userId) where.userId = query.userId;
137
+ if (query.action) where.action = query.action;
138
+ if (query.resourceType) where.resourceType = query.resourceType;
139
+ if (query.resourceId) where.resourceId = query.resourceId;
140
+
141
+ if (query.startDate || query.endDate) {
142
+ where.timestamp = {};
143
+ if (query.startDate) (where.timestamp as Record<string, unknown>).gte = query.startDate;
144
+ if (query.endDate) (where.timestamp as Record<string, unknown>).lte = query.endDate;
145
+ }
146
+
147
+ const take = Math.min(query.limit ?? 50, 100);
148
+
149
+ return prisma.auditLog.findMany({
150
+ where,
151
+ orderBy: { timestamp: 'desc' },
152
+ take,
153
+ ...(query.cursor ? { skip: 1, cursor: { id: query.cursor } } : {}),
154
+ include: {
155
+ user: { select: { id: true, email: true, name: true } },
156
+ },
157
+ });
158
+ }
159
+
160
+ export async function cleanupOldAuditLogs(retentionDays: number = 90): Promise<number> {
161
+ const cutoff = new Date();
162
+ cutoff.setDate(cutoff.getDate() - retentionDays);
163
+
164
+ const result = await prisma.auditLog.deleteMany({
165
+ where: { timestamp: { lt: cutoff } },
166
+ });
167
+
168
+ apiLogger.info({ deleted: result.count, retentionDays }, 'Audit log cleanup completed');
169
+ return result.count;
170
+ }
171
+ ```
172
+
173
+ ## Step 4: Usage in API Routes
174
+
175
+ Integrate audit logging into existing route handlers:
176
+
177
+ ```typescript
178
+ // Example: in an existing update route
179
+ import { logAuditEventFromRequest } from '@/features/audit-log/server';
180
+
181
+ export const PUT = withAuth(async (request, { params }) => {
182
+ try {
183
+ const { id } = await params;
184
+ const body = updateSchema.parse(await request.json());
185
+ const updated = await updateResource(request.session.userId, id, body);
186
+
187
+ await logAuditEventFromRequest(request, {
188
+ userId: request.session.userId,
189
+ action: 'update',
190
+ resourceType: 'widget',
191
+ resourceId: id,
192
+ metadata: { fields: Object.keys(body) },
193
+ });
194
+
195
+ return NextResponse.json(updated);
196
+ } catch (error) {
197
+ return handleApiError(error, { endpoint: '/api/protected/widgets/[id]' });
198
+ }
199
+ });
200
+ ```
201
+
202
+ ### Auth event logging
203
+
204
+ ```typescript
205
+ // In login route: src/app/api/auth/login/route.ts
206
+ await logAuditEventFromRequest(request, {
207
+ userId: user.id,
208
+ action: 'login',
209
+ resourceType: 'session',
210
+ });
211
+
212
+ // On failed login
213
+ await logAuditEventFromRequest(request, {
214
+ action: 'login_failed',
215
+ resourceType: 'auth',
216
+ metadata: { email: body.email },
217
+ });
218
+ ```
219
+
220
+ ## Step 5: Admin Audit Log API
221
+
222
+ ```typescript
223
+ // src/app/api/protected/admin/audit-logs/route.ts
224
+ import { handleApiError, withRole } from '@/lib/mars';
225
+ import { queryAuditLogs } from '@/features/audit-log/server';
226
+ import { NextResponse } from 'next/server';
227
+ import { z } from 'zod';
228
+
229
+ const querySchema = z.object({
230
+ userId: z.string().optional(),
231
+ action: z.string().optional(),
232
+ resourceType: z.string().optional(),
233
+ resourceId: z.string().optional(),
234
+ startDate: z.coerce.date().optional(),
235
+ endDate: z.coerce.date().optional(),
236
+ limit: z.coerce.number().int().min(1).max(100).optional(),
237
+ cursor: z.string().optional(),
238
+ });
239
+
240
+ export const GET = withRole(['admin'], async (request) => {
241
+ try {
242
+ const url = new URL(request.url);
243
+ const query = querySchema.parse(Object.fromEntries(url.searchParams));
244
+ const logs = await queryAuditLogs(query);
245
+ return NextResponse.json(logs);
246
+ } catch (error) {
247
+ return handleApiError(error, { endpoint: '/api/protected/admin/audit-logs' });
248
+ }
249
+ });
250
+ ```
251
+
252
+ ## Step 6: Cleanup API (Admin)
253
+
254
+ ```typescript
255
+ // src/app/api/protected/admin/audit-logs/cleanup/route.ts
256
+ import { handleApiError, withRole } from '@/lib/mars';
257
+ import { cleanupOldAuditLogs } from '@/features/audit-log/server';
258
+ import { NextResponse } from 'next/server';
259
+ import { z } from 'zod';
260
+
261
+ const cleanupSchema = z.object({
262
+ retentionDays: z.number().int().min(1).max(365).default(90),
263
+ });
264
+
265
+ export const POST = withRole(['admin'], async (request) => {
266
+ try {
267
+ const body = cleanupSchema.parse(await request.json());
268
+ const deleted = await cleanupOldAuditLogs(body.retentionDays);
269
+ return NextResponse.json({ deleted, retentionDays: body.retentionDays });
270
+ } catch (error) {
271
+ return handleApiError(error, { endpoint: '/api/protected/admin/audit-logs/cleanup' });
272
+ }
273
+ });
274
+ ```
275
+
276
+ ## Step 7: Retention Policy
277
+
278
+ Set up automated cleanup using a cron job or scheduled function:
279
+
280
+ ```typescript
281
+ // src/app/api/cron/audit-cleanup/route.ts
282
+ import { NextResponse } from 'next/server';
283
+ import { constantTimeEqual } from '@mars-stack/core/auth/crypto-utils';
284
+ import { cleanupOldAuditLogs } from '@/features/audit-log/server';
285
+
286
+ export async function GET(request: Request) {
287
+ const authHeader = request.headers.get('authorization') ?? '';
288
+ const expected = `Bearer ${process.env.CRON_SECRET}`;
289
+ if (!constantTimeEqual(authHeader, expected)) {
290
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
291
+ }
292
+
293
+ const deleted = await cleanupOldAuditLogs(90);
294
+ return NextResponse.json({ deleted });
295
+ }
296
+ ```
297
+
298
+ Add to `vercel.json` for Vercel Cron:
299
+
300
+ ```json
301
+ {
302
+ "crons": [
303
+ {
304
+ "path": "/api/cron/audit-cleanup",
305
+ "schedule": "0 3 * * 0"
306
+ }
307
+ ]
308
+ }
309
+ ```
310
+
311
+ ## Testing
312
+
313
+ ```typescript
314
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
315
+ import { logAuditEvent, queryAuditLogs } from './index';
316
+
317
+ vi.mock('@/lib/prisma', () => ({
318
+ prisma: {
319
+ auditLog: {
320
+ create: vi.fn(),
321
+ findMany: vi.fn(),
322
+ deleteMany: vi.fn(),
323
+ },
324
+ },
325
+ }));
326
+
327
+ vi.mock('@/lib/mars', () => ({
328
+ apiLogger: { error: vi.fn(), info: vi.fn() },
329
+ }));
330
+
331
+ describe('logAuditEvent', () => {
332
+ it('creates an audit log entry', async () => {
333
+ const { prisma } = await import('@/lib/prisma');
334
+ await logAuditEvent({
335
+ userId: 'user-1',
336
+ action: 'create',
337
+ resourceType: 'widget',
338
+ resourceId: 'widget-1',
339
+ });
340
+ expect(prisma.auditLog.create).toHaveBeenCalledWith(
341
+ expect.objectContaining({
342
+ data: expect.objectContaining({
343
+ userId: 'user-1',
344
+ action: 'create',
345
+ resourceType: 'widget',
346
+ }),
347
+ }),
348
+ );
349
+ });
350
+
351
+ it('does not throw if database write fails', async () => {
352
+ const { prisma } = await import('@/lib/prisma');
353
+ (prisma.auditLog.create as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('DB error'));
354
+ await expect(logAuditEvent({
355
+ action: 'login',
356
+ resourceType: 'session',
357
+ })).resolves.toBeUndefined();
358
+ });
359
+ });
360
+ ```
361
+
362
+ ## Checklist
363
+
364
+ - [ ] `AuditLog` model in Prisma schema with proper indexes
365
+ - [ ] User relation added (with `onDelete: SetNull`)
366
+ - [ ] `logAuditEvent()` and `logAuditEventFromRequest()` helpers created
367
+ - [ ] Server module imports `'server-only'`
368
+ - [ ] Audit logging integrated into key routes (auth, CRUD, admin actions)
369
+ - [ ] Admin query API with filtering and cursor pagination
370
+ - [ ] Cleanup endpoint and retention policy (default 90 days)
371
+ - [ ] Cron job for automated cleanup
372
+ - [ ] Audit logger never throws (fire-and-forget with error logging)
373
+ - [ ] No PII logged in metadata (no passwords, tokens, or full request bodies)
374
+ - [ ] `db:push` run after schema changes
375
+ - [ ] Tests written for audit log service