betterstart-cli 0.0.11 → 0.0.13
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/dist/assets/adapters/next/plugins/blog/plugin.ts +1 -1
- package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +115 -0
- package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +2 -7
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +20 -17
- package/dist/assets/adapters/next/templates/init/data/navigation.ts +2 -1
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-slash-menu.ts +28 -2
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor.ts +22 -1
- package/dist/assets/adapters/next/templates/init/hooks/use-audit-log.ts +23 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +63 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +97 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +17 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +37 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/register.ts +13 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/request-metadata.ts +37 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +72 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +19 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +23 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +21 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +23 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +17 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +19 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +23 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +23 -1
- package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +49 -0
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/index.ts +6 -1
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +36 -1
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +116 -12
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +33 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +96 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +17 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-filters.tsx +152 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +279 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +19 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page.tsx +14 -0
- package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/utils/audit/audit.ts +54 -0
- package/dist/assets/adapters/next/templates/init/utils/editor/tiptap.ts +0 -24
- package/dist/assets/shared-assets/react-admin/custom/content-editor/horizontal-rule-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/index.tsx +6 -0
- package/dist/assets/shared-assets/react-admin/custom/content-editor/main-toolbar-content.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-bubble-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-editor-controls.tsx +27 -40
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-popover-button.tsx +0 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/mobile-toolbar-content.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/mode-toggle-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +45 -8
- package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +153 -27
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode-dropdown-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +9 -9
- package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/table-button.tsx +9 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/blockquote-button/blockquote-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/code-block-button/code-block-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-content.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +11 -2
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/source-color-highlight-popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +9 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +2 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-main.tsx +19 -45
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +9 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/mark-button/mark-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/undo-redo-button/undo-redo-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/sortable-gallery-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/schema.json +0 -23
- package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/button.tsx +18 -18
- package/dist/assets/shared-assets/react-admin/ui/card.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +29 -5
- package/dist/assets/shared-assets/react-admin/ui/command.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +7 -3
- package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +32 -17
- package/dist/assets/shared-assets/react-admin/ui/empty.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/hover-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/item.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +7 -7
- package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/select.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
- package/dist/cli.js +2779 -810
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getRegisteredHooks } from './registry'
|
|
1
|
+
import { getRegisteredAdminHooks, getRegisteredHooks } from './registry'
|
|
2
2
|
import type {
|
|
3
|
+
AdminEventContext,
|
|
3
4
|
AfterLifecycleHookEvent,
|
|
4
5
|
BeforeLifecycleHookEvent,
|
|
5
6
|
LifecycleHookAbort,
|
|
@@ -33,19 +34,122 @@ export async function runBeforeHooks(
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
function getAdminActionFromLifecycleEvent(
|
|
38
|
+
event: AfterLifecycleHookEvent
|
|
39
|
+
): AdminEventContext['action'] | null {
|
|
40
|
+
if (event === 'afterCreate') return 'create'
|
|
41
|
+
if (event === 'afterUpdate') return 'update'
|
|
42
|
+
if (event === 'afterDelete') return 'delete'
|
|
43
|
+
if (event === 'afterPublish') return 'publish'
|
|
44
|
+
if (event === 'afterUnpublish') return 'unpublish'
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getTargetLabel(data: Record<string, unknown> | undefined): string | null {
|
|
49
|
+
if (!data) return null
|
|
50
|
+
|
|
51
|
+
for (const fieldName of ['title', 'name', 'label', 'slug', 'email', 'filename']) {
|
|
52
|
+
const value = data[fieldName]
|
|
53
|
+
if (typeof value === 'string' && value.trim()) return value
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getChangedFieldsFromLifecycleContext(
|
|
60
|
+
event: AfterLifecycleHookEvent,
|
|
61
|
+
context: LifecycleHookContext
|
|
62
|
+
): string[] {
|
|
63
|
+
if (context.changedFields) return context.changedFields
|
|
64
|
+
if (event === 'afterDelete') return []
|
|
65
|
+
|
|
66
|
+
return Object.keys(context.input ?? {})
|
|
67
|
+
.filter((fieldName) => fieldName !== 'id')
|
|
68
|
+
.sort((a, b) => a.localeCompare(b))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toAdminEventContext(
|
|
72
|
+
event: AfterLifecycleHookEvent,
|
|
73
|
+
context: LifecycleHookContext
|
|
74
|
+
): AdminEventContext | null {
|
|
75
|
+
const action = getAdminActionFromLifecycleEvent(event)
|
|
76
|
+
if (!action) return null
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
category: 'entity',
|
|
80
|
+
action,
|
|
81
|
+
targetType: context.entityType,
|
|
82
|
+
targetId: context.entityId ?? null,
|
|
83
|
+
targetLabel: context.targetLabel ?? getTargetLabel(context.data),
|
|
84
|
+
user: context.user,
|
|
85
|
+
timestamp: context.timestamp,
|
|
86
|
+
sourceType: context.sourceType ?? 'admin_ui',
|
|
87
|
+
ipAddress: context.ipAddress ?? null,
|
|
88
|
+
userAgent: context.userAgent ?? null,
|
|
89
|
+
changedFields: getChangedFieldsFromLifecycleContext(event, context),
|
|
90
|
+
metadata: {
|
|
91
|
+
entityType: context.entityType,
|
|
92
|
+
...(context.metadata ?? {})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function runSettledHook<Context>(
|
|
98
|
+
handler: (context: Context) => void | Promise<void>,
|
|
99
|
+
context: Context
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
try {
|
|
102
|
+
return Promise.resolve(handler(context))
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return Promise.reject(error)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function runAdminEventHooks(context: AdminEventContext): void {
|
|
109
|
+
const handlers = getRegisteredAdminHooks({
|
|
110
|
+
category: context.category,
|
|
111
|
+
action: context.action
|
|
112
|
+
})
|
|
38
113
|
if (handlers.length === 0) return
|
|
39
114
|
|
|
40
|
-
void Promise.allSettled(handlers.map((handler) => handler
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
115
|
+
void Promise.allSettled(handlers.map((handler) => runSettledHook(handler, context))).then(
|
|
116
|
+
(results) => {
|
|
117
|
+
for (const result of results) {
|
|
118
|
+
if (result.status === 'rejected') {
|
|
119
|
+
console.error('Admin event hook failed:', {
|
|
120
|
+
category: context.category,
|
|
121
|
+
action: context.action,
|
|
122
|
+
targetType: context.targetType,
|
|
123
|
+
targetId: context.targetId,
|
|
124
|
+
error: result.reason
|
|
125
|
+
})
|
|
126
|
+
}
|
|
48
127
|
}
|
|
49
128
|
}
|
|
50
|
-
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function runAfterHooks(event: AfterLifecycleHookEvent, context: LifecycleHookContext): void {
|
|
133
|
+
const handlers = getRegisteredHooks(context.entityType, event)
|
|
134
|
+
|
|
135
|
+
if (handlers.length > 0) {
|
|
136
|
+
void Promise.allSettled(handlers.map((handler) => runSettledHook(handler, context))).then(
|
|
137
|
+
(results) => {
|
|
138
|
+
for (const result of results) {
|
|
139
|
+
if (result.status === 'rejected') {
|
|
140
|
+
console.error('Lifecycle hook failed:', {
|
|
141
|
+
entityType: context.entityType,
|
|
142
|
+
event,
|
|
143
|
+
error: result.reason
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const adminEvent = toAdminEventContext(event, context)
|
|
152
|
+
if (adminEvent) {
|
|
153
|
+
runAdminEventHooks(adminEvent)
|
|
154
|
+
}
|
|
51
155
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuditLogAction,
|
|
3
|
+
AuditLogCategory,
|
|
4
|
+
AuditLogSourceType
|
|
5
|
+
} from '@admin/actions/audit/types'
|
|
1
6
|
import type { User } from '@admin/auth'
|
|
2
7
|
|
|
3
8
|
export type LifecycleHookEvent =
|
|
@@ -22,6 +27,32 @@ export interface LifecycleHookContext {
|
|
|
22
27
|
entityId?: string | undefined
|
|
23
28
|
input?: Record<string, unknown> | undefined
|
|
24
29
|
data?: Record<string, unknown> | undefined
|
|
30
|
+
changedFields?: string[] | undefined
|
|
31
|
+
sourceType?: AuditLogSourceType | undefined
|
|
32
|
+
ipAddress?: string | null | undefined
|
|
33
|
+
userAgent?: string | null | undefined
|
|
34
|
+
targetLabel?: string | null | undefined
|
|
35
|
+
metadata?: Record<string, unknown> | undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AdminEventDescriptor {
|
|
39
|
+
category?: AuditLogCategory
|
|
40
|
+
action?: AuditLogAction
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AdminEventContext {
|
|
44
|
+
category: AuditLogCategory
|
|
45
|
+
action: AuditLogAction
|
|
46
|
+
targetType: string
|
|
47
|
+
targetId?: string | null | undefined
|
|
48
|
+
targetLabel?: string | null | undefined
|
|
49
|
+
user?: User | null | undefined
|
|
50
|
+
timestamp: string
|
|
51
|
+
sourceType?: AuditLogSourceType | undefined
|
|
52
|
+
ipAddress?: string | null | undefined
|
|
53
|
+
userAgent?: string | null | undefined
|
|
54
|
+
changedFields?: string[] | undefined
|
|
55
|
+
metadata?: Record<string, unknown> | undefined
|
|
25
56
|
}
|
|
26
57
|
|
|
27
58
|
export interface LifecycleHookAbort {
|
|
@@ -37,3 +68,5 @@ export type AfterLifecycleHookHandler = (context: LifecycleHookContext) => void
|
|
|
37
68
|
|
|
38
69
|
export type LifecycleHookHandler<Event extends LifecycleHookEvent> =
|
|
39
70
|
Event extends BeforeLifecycleHookEvent ? BeforeLifecycleHookHandler : AfterLifecycleHookHandler
|
|
71
|
+
|
|
72
|
+
export type AdminEventHookHandler = (context: AdminEventContext) => void | Promise<void>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AuditLogEntry } from '@admin/actions/audit'
|
|
4
|
+
import { Badge } from '@admin/components/ui/badge'
|
|
5
|
+
import {
|
|
6
|
+
Drawer,
|
|
7
|
+
DrawerContent,
|
|
8
|
+
DrawerDescription,
|
|
9
|
+
DrawerHeader,
|
|
10
|
+
DrawerTitle
|
|
11
|
+
} from '@admin/components/ui/drawer'
|
|
12
|
+
import { ScrollArea } from '@admin/components/ui/scroll-area'
|
|
13
|
+
import { AuditLogDetailRow } from './audit-log-detail-row'
|
|
14
|
+
|
|
15
|
+
interface AuditLogDetailDrawerProps {
|
|
16
|
+
auditLog: AuditLogEntry | null
|
|
17
|
+
open: boolean
|
|
18
|
+
onOpenChange: (open: boolean) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatDateTime(value: string): string {
|
|
22
|
+
const date = new Date(value)
|
|
23
|
+
if (Number.isNaN(date.getTime())) return value
|
|
24
|
+
|
|
25
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
26
|
+
dateStyle: 'medium',
|
|
27
|
+
timeStyle: 'medium'
|
|
28
|
+
}).format(date)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatMetadata(metadata: Record<string, unknown> | null): string {
|
|
32
|
+
if (!metadata || Object.keys(metadata).length === 0) return '{}'
|
|
33
|
+
return JSON.stringify(metadata, null, 2)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AuditLogDetailDrawer({ auditLog, open, onOpenChange }: AuditLogDetailDrawerProps) {
|
|
37
|
+
return (
|
|
38
|
+
<Drawer direction="right" open={open} onOpenChange={onOpenChange} modal>
|
|
39
|
+
<DrawerContent className="h-full sm:max-w-xl">
|
|
40
|
+
<DrawerHeader>
|
|
41
|
+
<DrawerTitle>Audit Event</DrawerTitle>
|
|
42
|
+
<DrawerDescription>
|
|
43
|
+
{auditLog ? `${auditLog.category}.${auditLog.action}` : 'No event selected'}
|
|
44
|
+
</DrawerDescription>
|
|
45
|
+
</DrawerHeader>
|
|
46
|
+
<ScrollArea className="min-h-0 flex-1">
|
|
47
|
+
{auditLog ? (
|
|
48
|
+
<div className="p-4">
|
|
49
|
+
<dl className="rounded-lg border border-border bg-card px-4">
|
|
50
|
+
<AuditLogDetailRow label="Time" value={formatDateTime(auditLog.createdAt)} />
|
|
51
|
+
<AuditLogDetailRow label="Category" value={auditLog.category} />
|
|
52
|
+
<AuditLogDetailRow label="Action" value={auditLog.action} />
|
|
53
|
+
<AuditLogDetailRow
|
|
54
|
+
label="Target"
|
|
55
|
+
value={auditLog.targetLabel ?? auditLog.targetId}
|
|
56
|
+
/>
|
|
57
|
+
<AuditLogDetailRow label="Target Type" value={auditLog.targetType} />
|
|
58
|
+
<AuditLogDetailRow label="Target ID" value={auditLog.targetId} />
|
|
59
|
+
<AuditLogDetailRow
|
|
60
|
+
label="Actor"
|
|
61
|
+
value={auditLog.actorEmail ?? auditLog.actorName ?? auditLog.actorUserId}
|
|
62
|
+
/>
|
|
63
|
+
<AuditLogDetailRow label="Actor Role" value={auditLog.actorRole} />
|
|
64
|
+
<AuditLogDetailRow label="Source" value={auditLog.sourceType} />
|
|
65
|
+
<AuditLogDetailRow label="IP" value={auditLog.ipAddress} />
|
|
66
|
+
<AuditLogDetailRow label="User Agent" value={auditLog.userAgent} />
|
|
67
|
+
<AuditLogDetailRow
|
|
68
|
+
label="Fields"
|
|
69
|
+
value={
|
|
70
|
+
auditLog.changedFields.length > 0 ? (
|
|
71
|
+
<div className="flex flex-wrap gap-1">
|
|
72
|
+
{auditLog.changedFields.map((field) => (
|
|
73
|
+
<Badge key={field} variant="secondary">
|
|
74
|
+
{field}
|
|
75
|
+
</Badge>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
) : (
|
|
79
|
+
'None'
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
/>
|
|
83
|
+
</dl>
|
|
84
|
+
<div className="mt-4 rounded-lg border border-border bg-card p-4">
|
|
85
|
+
<h3 className="mb-2 text-sm font-medium">Metadata</h3>
|
|
86
|
+
<pre className="max-h-72 overflow-auto rounded-md bg-muted p-3 text-xs leading-relaxed">
|
|
87
|
+
{formatMetadata(auditLog.metadata)}
|
|
88
|
+
</pre>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
) : null}
|
|
92
|
+
</ScrollArea>
|
|
93
|
+
</DrawerContent>
|
|
94
|
+
</Drawer>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
interface AuditLogDetailRowProps {
|
|
4
|
+
label: string
|
|
5
|
+
value: ReactNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function AuditLogDetailRow({ label, value }: AuditLogDetailRowProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 border-b border-border/70 py-3 last:border-b-0">
|
|
11
|
+
<dt className="text-xs font-medium uppercase tracking-normal text-muted-foreground">
|
|
12
|
+
{label}
|
|
13
|
+
</dt>
|
|
14
|
+
<dd className="min-w-0 text-sm text-foreground break-words">{value || 'Unknown'}</dd>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AUDIT_LOG_ACTIONS,
|
|
5
|
+
AUDIT_LOG_CATEGORIES,
|
|
6
|
+
type AuditLogAction,
|
|
7
|
+
type AuditLogCategory
|
|
8
|
+
} from '@admin/actions/audit/types'
|
|
9
|
+
import { Button } from '@admin/components/ui/button'
|
|
10
|
+
import { Input } from '@admin/components/ui/input'
|
|
11
|
+
import {
|
|
12
|
+
Select,
|
|
13
|
+
SelectContent,
|
|
14
|
+
SelectItem,
|
|
15
|
+
SelectTrigger,
|
|
16
|
+
SelectValue
|
|
17
|
+
} from '@admin/components/ui/select'
|
|
18
|
+
import { RotateCcw } from 'lucide-react'
|
|
19
|
+
|
|
20
|
+
const ALL_FILTER_VALUE = '_all'
|
|
21
|
+
|
|
22
|
+
interface AuditLogFiltersProps {
|
|
23
|
+
search: string
|
|
24
|
+
category: string
|
|
25
|
+
action: string
|
|
26
|
+
targetType: string
|
|
27
|
+
actor: string
|
|
28
|
+
createdAtFrom: string
|
|
29
|
+
createdAtTo: string
|
|
30
|
+
activeFilterCount: number
|
|
31
|
+
onSearchChange: (value: string) => void
|
|
32
|
+
onCategoryChange: (value: AuditLogCategory | '') => void
|
|
33
|
+
onActionChange: (value: AuditLogAction | '') => void
|
|
34
|
+
onTargetTypeChange: (value: string) => void
|
|
35
|
+
onActorChange: (value: string) => void
|
|
36
|
+
onCreatedAtFromChange: (value: string) => void
|
|
37
|
+
onCreatedAtToChange: (value: string) => void
|
|
38
|
+
onClear: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatFilterLabel(value: string): string {
|
|
42
|
+
return value
|
|
43
|
+
.split('_')
|
|
44
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
45
|
+
.join(' ')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function AuditLogFilters({
|
|
49
|
+
search,
|
|
50
|
+
category,
|
|
51
|
+
action,
|
|
52
|
+
targetType,
|
|
53
|
+
actor,
|
|
54
|
+
createdAtFrom,
|
|
55
|
+
createdAtTo,
|
|
56
|
+
activeFilterCount,
|
|
57
|
+
onSearchChange,
|
|
58
|
+
onCategoryChange,
|
|
59
|
+
onActionChange,
|
|
60
|
+
onTargetTypeChange,
|
|
61
|
+
onActorChange,
|
|
62
|
+
onCreatedAtFromChange,
|
|
63
|
+
onCreatedAtToChange,
|
|
64
|
+
onClear
|
|
65
|
+
}: AuditLogFiltersProps) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex flex-col gap-2 border-b border-border bg-background px-4 py-3">
|
|
68
|
+
<div className="grid gap-2 md:grid-cols-[minmax(180px,1.2fr)_repeat(5,minmax(132px,1fr))_auto]">
|
|
69
|
+
<Input
|
|
70
|
+
value={search}
|
|
71
|
+
onChange={(event) => onSearchChange(event.target.value)}
|
|
72
|
+
placeholder="Search logs"
|
|
73
|
+
className="h-8"
|
|
74
|
+
/>
|
|
75
|
+
<Select
|
|
76
|
+
value={category || ALL_FILTER_VALUE}
|
|
77
|
+
onValueChange={(value) =>
|
|
78
|
+
onCategoryChange(value === ALL_FILTER_VALUE ? '' : (value as AuditLogCategory))
|
|
79
|
+
}
|
|
80
|
+
>
|
|
81
|
+
<SelectTrigger className="h-8">
|
|
82
|
+
<SelectValue placeholder="Category" />
|
|
83
|
+
</SelectTrigger>
|
|
84
|
+
<SelectContent>
|
|
85
|
+
<SelectItem value={ALL_FILTER_VALUE}>All categories</SelectItem>
|
|
86
|
+
{AUDIT_LOG_CATEGORIES.map((value) => (
|
|
87
|
+
<SelectItem key={value} value={value}>
|
|
88
|
+
{formatFilterLabel(value)}
|
|
89
|
+
</SelectItem>
|
|
90
|
+
))}
|
|
91
|
+
</SelectContent>
|
|
92
|
+
</Select>
|
|
93
|
+
<Select
|
|
94
|
+
value={action || ALL_FILTER_VALUE}
|
|
95
|
+
onValueChange={(value) =>
|
|
96
|
+
onActionChange(value === ALL_FILTER_VALUE ? '' : (value as AuditLogAction))
|
|
97
|
+
}
|
|
98
|
+
>
|
|
99
|
+
<SelectTrigger className="h-8">
|
|
100
|
+
<SelectValue placeholder="Action" />
|
|
101
|
+
</SelectTrigger>
|
|
102
|
+
<SelectContent>
|
|
103
|
+
<SelectItem value={ALL_FILTER_VALUE}>All actions</SelectItem>
|
|
104
|
+
{AUDIT_LOG_ACTIONS.map((value) => (
|
|
105
|
+
<SelectItem key={value} value={value}>
|
|
106
|
+
{formatFilterLabel(value)}
|
|
107
|
+
</SelectItem>
|
|
108
|
+
))}
|
|
109
|
+
</SelectContent>
|
|
110
|
+
</Select>
|
|
111
|
+
<Input
|
|
112
|
+
value={targetType}
|
|
113
|
+
onChange={(event) => onTargetTypeChange(event.target.value)}
|
|
114
|
+
placeholder="Target type"
|
|
115
|
+
className="h-8"
|
|
116
|
+
/>
|
|
117
|
+
<Input
|
|
118
|
+
value={actor}
|
|
119
|
+
onChange={(event) => onActorChange(event.target.value)}
|
|
120
|
+
placeholder="Actor"
|
|
121
|
+
className="h-8"
|
|
122
|
+
/>
|
|
123
|
+
<div className="grid grid-cols-2 gap-2 md:col-span-2">
|
|
124
|
+
<Input
|
|
125
|
+
type="date"
|
|
126
|
+
value={createdAtFrom}
|
|
127
|
+
onChange={(event) => onCreatedAtFromChange(event.target.value)}
|
|
128
|
+
className="h-8"
|
|
129
|
+
aria-label="Audit log start date"
|
|
130
|
+
/>
|
|
131
|
+
<Input
|
|
132
|
+
type="date"
|
|
133
|
+
value={createdAtTo}
|
|
134
|
+
onChange={(event) => onCreatedAtToChange(event.target.value)}
|
|
135
|
+
className="h-8"
|
|
136
|
+
aria-label="Audit log end date"
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
<Button
|
|
140
|
+
type="button"
|
|
141
|
+
variant="outline"
|
|
142
|
+
size="icon-sm"
|
|
143
|
+
disabled={activeFilterCount === 0}
|
|
144
|
+
onClick={onClear}
|
|
145
|
+
aria-label="Clear audit log filters"
|
|
146
|
+
>
|
|
147
|
+
<RotateCcw />
|
|
148
|
+
</Button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|