betterstart-cli 0.0.116 → 0.0.118

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 (85) hide show
  1. package/dist/assets/adapters/next/templates/init/admin-globals.css +20 -0
  2. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid-row.tsx +33 -1
  3. package/dist/assets/adapters/next/templates/init/components/shared/media/media-gallery-dialog.tsx +3 -3
  4. package/dist/assets/adapters/next/templates/init/components/shared/media/media-preview.tsx +3 -1
  5. package/dist/assets/adapters/next/templates/init/hooks/use-copy-to-clipboard.ts +1 -3
  6. package/dist/assets/adapters/next/templates/init/hooks/use-field-error-visible.ts +25 -0
  7. package/dist/assets/adapters/next/templates/init/hooks/use-media.ts +1 -1
  8. package/dist/assets/adapters/next/templates/init/hooks/use-upload.ts +11 -5
  9. package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +1 -0
  10. package/dist/assets/adapters/next/templates/init/lib/actions/audit/query-audit-logs.ts +2 -2
  11. package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +1 -2
  12. package/dist/assets/adapters/next/templates/init/lib/actions/auth/client.ts +4 -1
  13. package/dist/assets/adapters/next/templates/init/lib/actions/entity-versions/get-entity-versions.ts +5 -1
  14. package/dist/assets/adapters/next/templates/init/lib/actions/forms/get-all-form-settings.ts +1 -1
  15. package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +2 -1
  16. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +1 -0
  17. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +1 -0
  18. package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +2 -4
  19. package/dist/assets/adapters/next/templates/init/lib/actions/profile/change-own-password.ts +1 -0
  20. package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +1 -0
  21. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +1 -0
  22. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +16 -6
  23. package/dist/assets/adapters/next/templates/init/lib/actions/users/change-user-password.ts +1 -0
  24. package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +2 -1
  25. package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +1 -0
  26. package/dist/assets/adapters/next/templates/init/lib/actions/users/types.ts +1 -1
  27. package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +1 -0
  28. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +12 -4
  29. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +4 -4
  30. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +3 -7
  31. package/dist/assets/adapters/next/templates/init/pages/container-layout.tsx +3 -0
  32. package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +78 -76
  33. package/dist/assets/adapters/next/templates/init/pages/forgot-password-form.tsx +5 -8
  34. package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +2 -2
  35. package/dist/assets/adapters/next/templates/init/pages/login-form.tsx +7 -9
  36. package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +2 -2
  37. package/dist/assets/adapters/next/templates/init/pages/reset-password-form.tsx +8 -16
  38. package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +2 -2
  39. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-detail-row.tsx +1 -1
  40. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +1 -6
  41. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-table.tsx +2 -7
  42. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +1 -6
  43. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +1 -1
  44. package/dist/assets/adapters/next/templates/init/public/placeholder.png +0 -0
  45. package/dist/assets/adapters/next/templates/init/types/index.ts +2 -0
  46. package/dist/assets/adapters/next/templates/init/utils/audit/normalize-comparable-value.ts +4 -2
  47. package/dist/assets/adapters/next/templates/init/utils/date/format-action-date.ts +12 -0
  48. package/dist/assets/adapters/next/templates/init/utils/dev-mode/dev-mode-json-schemas.ts +66 -0
  49. package/dist/assets/adapters/next/templates/init/utils/dev-mode/parse-dev-mode-lifecycle-hooks.ts +11 -0
  50. package/dist/assets/adapters/next/templates/init/utils/dev-mode/parse-dev-mode-snippets.ts +9 -0
  51. package/dist/assets/adapters/next/templates/init/utils/editor/set-task-item-checked.ts +13 -0
  52. package/dist/assets/adapters/next/templates/init/utils/editor/update-nodes-attr.ts +2 -1
  53. package/dist/assets/adapters/next/templates/init/utils/email/email-theme.ts +13 -0
  54. package/dist/assets/adapters/next/templates/init/utils/email/render-form-submission-rows.ts +3 -2
  55. package/dist/assets/adapters/next/templates/init/utils/email/render-notification-email.ts +7 -6
  56. package/dist/assets/adapters/next/templates/init/utils/email/render-thank-you-email.ts +5 -4
  57. package/dist/assets/adapters/next/templates/init/utils/error/read-string-property.ts +4 -2
  58. package/dist/assets/adapters/next/templates/init/utils/error/recognize.ts +2 -4
  59. package/dist/assets/adapters/next/templates/init/utils/error/to-action-error.ts +0 -1
  60. package/dist/assets/adapters/next/templates/init/utils/form/clone-default-value.ts +4 -2
  61. package/dist/assets/adapters/next/templates/init/utils/shared/is-record.ts +3 -0
  62. package/dist/assets/adapters/next/templates/init/utils/table/default-get-id.ts +3 -2
  63. package/dist/assets/shared-assets/react-admin/custom/content-editor/editor-toolbar.tsx +1 -1
  64. package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +5 -3
  65. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +1 -1
  66. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-node/removable-image-node/removable-image-node.tsx +3 -1
  67. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-node/task-item-node/task-item-node-extension.tsx +6 -15
  68. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +1 -6
  69. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +1 -6
  70. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +9 -2
  71. package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +3 -3
  72. package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +2 -2
  73. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +3 -1
  74. package/dist/assets/shared-assets/react-admin/custom/progressive-blur.tsx +3 -1
  75. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +20 -23
  76. package/dist/assets/shared-assets/react-admin/ui/form.tsx +6 -3
  77. package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +1 -1
  78. package/dist/{chunk-SG4U6EQY.js → chunk-BNHFSHPE.js} +14 -1
  79. package/dist/chunk-BNHFSHPE.js.map +1 -0
  80. package/dist/cli.js +962 -633
  81. package/dist/cli.js.map +1 -1
  82. package/dist/{read-namespaced-template-D2CD4Q7U.js → read-namespaced-template-WYIGUH7S.js} +2 -2
  83. package/package.json +1 -1
  84. package/dist/chunk-SG4U6EQY.js.map +0 -1
  85. /package/dist/{read-namespaced-template-D2CD4Q7U.js.map → read-namespaced-template-WYIGUH7S.js.map} +0 -0
@@ -80,6 +80,26 @@
80
80
  );
81
81
  }
82
82
 
83
+ @theme {
84
+ --breakpoint-xs: 480px;
85
+ --spacing-entity-editor: 300px;
86
+ --spacing-table-cell: 300px;
87
+ --spacing-form-settings-name: 200px;
88
+ --spacing-form-settings-recipients: 240px;
89
+ --spacing-auth-login-panel: 540px;
90
+ --spacing-auth-reset-panel: 520px;
91
+ --spacing-auth-forgot-panel: 460px;
92
+ --spacing-media-dialog: 700px;
93
+ --spacing-media-tile: 180px;
94
+ --spacing-media-thumb: 100px;
95
+ --spacing-gallery-item: 184px;
96
+ --spacing-date-range-trigger: 206px;
97
+ --spacing-date-range-popover: 282px;
98
+ --spacing-source-editor: 240px;
99
+ --grid-template-columns-entity-form: 1fr 360px;
100
+ --grid-template-columns-audit-detail: 120px 1fr;
101
+ }
102
+
83
103
  :root {
84
104
  --card: oklch(1 0 0);
85
105
  --card-foreground: oklch(0.145 0 0);
@@ -3,7 +3,37 @@
3
3
  import type * as React from 'react'
4
4
  import { cn } from '@admin/utils/shared/cn'
5
5
 
6
- function DataGridRow({ className, style, ...props }: React.ComponentProps<'div'>) {
6
+ const INTERACTIVE_ROW_SELECTOR = 'button, a, input, [role="checkbox"], [role="switch"]'
7
+
8
+ type DataGridRowProps = React.ComponentProps<'div'> & {
9
+ onActivate?: () => void
10
+ }
11
+
12
+ function DataGridRow({
13
+ className,
14
+ style,
15
+ onActivate,
16
+ onClick,
17
+ onKeyDown,
18
+ ...props
19
+ }: DataGridRowProps) {
20
+ function handleClick(event: React.MouseEvent<HTMLDivElement>) {
21
+ onClick?.(event)
22
+ const target = event.target
23
+ if (!onActivate || !(target instanceof HTMLElement)) return
24
+ if (!event.currentTarget.contains(target)) return
25
+ if (target.closest(INTERACTIVE_ROW_SELECTOR)) return
26
+ onActivate()
27
+ }
28
+
29
+ function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
30
+ onKeyDown?.(event)
31
+ if (!onActivate || event.target !== event.currentTarget) return
32
+ if (event.key !== 'Enter' && event.key !== ' ') return
33
+ event.preventDefault()
34
+ onActivate()
35
+ }
36
+
7
37
  return (
8
38
  <div
9
39
  role="row"
@@ -14,6 +44,8 @@ function DataGridRow({ className, style, ...props }: React.ComponentProps<'div'>
14
44
  )}
15
45
  style={{ ...style, gridColumn: '1 / -1', gridTemplateColumns: 'subgrid' }}
16
46
  tabIndex={-1}
47
+ onClick={handleClick}
48
+ onKeyDown={handleKeyDown}
17
49
  {...props}
18
50
  />
19
51
  )
@@ -99,7 +99,7 @@ export function MediaGalleryDialog({
99
99
  onOpenChange(next)
100
100
  }}
101
101
  >
102
- <DialogContent className="flex h-[80svh] max-h-[700px] min-h-0 max-w-3xl flex-col gap-0 overflow-hidden">
102
+ <DialogContent className="flex h-[80svh] max-h-media-dialog min-h-0 max-w-3xl flex-col gap-0 overflow-hidden">
103
103
  <DialogHeader>
104
104
  <DialogTitle>Select file</DialogTitle>
105
105
  <DialogDescription>
@@ -128,7 +128,7 @@ export function MediaGalleryDialog({
128
128
  progress={progress}
129
129
  isUploading={isUploading}
130
130
  accept={accept}
131
- className="h-[180px]"
131
+ className="h-media-tile"
132
132
  />
133
133
  </TabsUi.TabsContent>
134
134
 
@@ -136,7 +136,7 @@ export function MediaGalleryDialog({
136
136
  <MediaUrlImporter
137
137
  onImportFromUrl={uploadFromUrl}
138
138
  accept={accept}
139
- className="h-[180px]"
139
+ className="h-media-tile"
140
140
  />
141
141
  </TabsUi.TabsContent>
142
142
  </TabsUi.Tabs>
@@ -12,6 +12,8 @@ interface MediaPreviewProps {
12
12
  showMetadata?: boolean
13
13
  }
14
14
 
15
+ const MEDIA_PREVIEW_IMAGE_SIZES = '200px'
16
+
15
17
  export function MediaPreview({ media, isFilled = false, showMetadata = false }: MediaPreviewProps) {
16
18
  const { hasImageFailed, markImageFailed } = useFailedImageUrl(media.url)
17
19
 
@@ -26,7 +28,7 @@ export function MediaPreview({ media, isFilled = false, showMetadata = false }:
26
28
  'object-contain': !isFilled
27
29
  })}
28
30
  fill
29
- sizes="200px"
31
+ sizes={MEDIA_PREVIEW_IMAGE_SIZES}
30
32
  unoptimized
31
33
  loading="lazy"
32
34
  decoding="async"
@@ -22,7 +22,6 @@ export function useCopyToClipboard(delay = 2000): [CopyFn, boolean] {
22
22
  clearResetTimer()
23
23
 
24
24
  if (typeof navigator === 'undefined' || !navigator.clipboard) {
25
- console.warn('Clipboard not supported')
26
25
  setIsCopied(false)
27
26
  return false
28
27
  }
@@ -35,8 +34,7 @@ export function useCopyToClipboard(delay = 2000): [CopyFn, boolean] {
35
34
  resetTimerRef.current = null
36
35
  }, delay)
37
36
  return true
38
- } catch (error) {
39
- console.warn('Copy failed', error)
37
+ } catch {
40
38
  setIsCopied(false)
41
39
  return false
42
40
  }
@@ -0,0 +1,25 @@
1
+ 'use client'
2
+
3
+ import type { FieldPath, FieldValues, UseFormReturn } from 'react-hook-form'
4
+ import { useWatch } from 'react-hook-form'
5
+
6
+ function hasValue(value: unknown): boolean {
7
+ if (value === null || value === undefined) return false
8
+ if (typeof value === 'string') return value.trim().length > 0
9
+ if (Array.isArray(value)) return value.length > 0
10
+
11
+ return true
12
+ }
13
+
14
+ export function useFieldErrorVisible<TValues extends FieldValues>(
15
+ form: UseFormReturn<TValues>,
16
+ name: FieldPath<TValues>
17
+ ): boolean {
18
+ const value = useWatch({ control: form.control, name })
19
+ const { error, isTouched } = form.getFieldState(name, form.formState)
20
+
21
+ if (!error) return false
22
+ if (form.formState.isSubmitted) return true
23
+
24
+ return isTouched && hasValue(value)
25
+ }
@@ -69,7 +69,7 @@ export function useMediaSelection(mode: 'single' | 'multi') {
69
69
  [mode]
70
70
  )
71
71
 
72
- const reset = React.useCallback(() => setSelectedMap(new Map()), [])
72
+ const reset = () => setSelectedMap(emptySelection())
73
73
 
74
74
  const getSelection = React.useCallback(() => {
75
75
  const values = [...selectedMap.values()]
@@ -190,11 +190,17 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
190
190
  formData.append('allowedTypes', validationConfig.allowedTypes.join(','))
191
191
  }
192
192
 
193
- const response = await fetch('/api/admin/upload', {
194
- method: 'POST',
195
- body: formData,
196
- signal: abortController.signal
197
- })
193
+ let response: Response
194
+ try {
195
+ response = await fetch('/api/admin/upload', {
196
+ method: 'POST',
197
+ body: formData,
198
+ signal: abortController.signal
199
+ })
200
+ } catch (error) {
201
+ if (error instanceof Error && error.name === 'AbortError') throw error
202
+ throw new Error('Upload failed. Check your connection and try again.')
203
+ }
198
204
 
199
205
  const result = (await response.json()) as UploadFileResult
200
206
 
@@ -51,6 +51,7 @@ export async function cleanupAuditLogs(): Promise<CleanupAuditLogsResult> {
51
51
 
52
52
  return { success: true, deletedCount: deletedRows.length, cutoff }
53
53
  } catch (error) {
54
+ console.error('Failed to clean up audit logs:', error)
54
55
  return {
55
56
  success: false,
56
57
  error: toActionError(error, 'Failed to clean up audit logs')
@@ -1,4 +1,4 @@
1
- import type { AuditLogEntry, AuditLogPage, AuditLogQuery } from '@admin/actions/audit/types'
1
+ import type { AuditLogPage, AuditLogQuery } from '@admin/actions/audit/types'
2
2
  import { clampPage } from '@admin/actions/audit/clamp-page'
3
3
  import { getAuditLogOrderBy } from '@admin/actions/audit/get-audit-log-order-by'
4
4
  import { normalizePageSize } from '@admin/actions/audit/normalize-page-size'
@@ -62,7 +62,7 @@ export async function queryAuditLogs(query: AuditLogQuery = {}): Promise<AuditLo
62
62
  const resolvedPageSize = pageSize ?? total
63
63
 
64
64
  return {
65
- auditLogs: rows as AuditLogEntry[],
65
+ auditLogs: rows,
66
66
  total,
67
67
  page,
68
68
  pageSize: resolvedPageSize,
@@ -6,7 +6,6 @@ import db from '@admin/db'
6
6
  import { auditLog } from '@admin/db/schema'
7
7
 
8
8
  const MAX_TARGET_TYPE_LENGTH = 100
9
- const MAX_SOURCE_TYPE_LENGTH = 50
10
9
 
11
10
  function truncate(value: string | null | undefined, maxLength: number): string | null {
12
11
  if (!value) return null
@@ -24,7 +23,7 @@ export async function recordAuditEvent(event: AdminEventContext): Promise<void>
24
23
  actorEmail: event.user?.email ?? null,
25
24
  actorName: event.user?.name ?? null,
26
25
  actorRole: typeof event.user?.role === 'string' ? event.user.role : null,
27
- sourceType: truncate(event.sourceType, MAX_SOURCE_TYPE_LENGTH) ?? 'server_action',
26
+ sourceType: event.sourceType ?? 'server_action',
28
27
  ipAddress: truncate(event.ipAddress, MAX_IP_LENGTH),
29
28
  userAgent: truncate(event.userAgent, MAX_USER_AGENT_LENGTH),
30
29
  changedFields: normalizeChangedFields(event.changedFields),
@@ -1,12 +1,15 @@
1
1
  'use client'
2
2
 
3
+ import type { auth } from '@admin/auth/server'
4
+ import { inferAdditionalFields } from 'better-auth/client/plugins'
3
5
  import { createAuthClient } from 'better-auth/react'
4
6
 
5
7
  export const authClient = createAuthClient({
6
8
  baseURL:
7
9
  process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL ||
8
10
  (typeof window !== 'undefined' ? window.location.origin : ''),
9
- basePath: '/api/admin/auth'
11
+ basePath: '/api/admin/auth',
12
+ plugins: [inferAdditionalFields<typeof auth>()]
10
13
  })
11
14
 
12
15
  export const { signIn, signUp, signOut, useSession } = authClient
@@ -3,6 +3,7 @@
3
3
  import type { EntityVersionWithAuthor } from './types'
4
4
  import db from '@admin/db'
5
5
  import { entityVersions, user } from '@admin/db/schema'
6
+ import { isRecord } from '@admin/utils/shared/is-record'
6
7
  import { and, desc, eq } from 'drizzle-orm'
7
8
  import { alias } from 'drizzle-orm/pg-core'
8
9
  import { cacheLife, cacheTag } from 'next/cache'
@@ -40,7 +41,10 @@ export async function getEntityVersions(
40
41
  .where(and(eq(entityVersions.entityType, entityType), eq(entityVersions.entityId, entityId)))
41
42
  .orderBy(desc(entityVersions.version), desc(entityVersions.createdAt))
42
43
 
43
- return rows as EntityVersionWithAuthor[]
44
+ return rows.map((row) => ({
45
+ ...row,
46
+ data: isRecord(row.data) ? row.data : {}
47
+ }))
44
48
  } catch (error) {
45
49
  console.error('Error fetching entity versions:', error)
46
50
  return []
@@ -13,7 +13,7 @@ export async function getAllFormSettings(): Promise<FormSettingsData[]> {
13
13
 
14
14
  try {
15
15
  const settings = await db.select().from(formSettings)
16
- return settings as FormSettingsData[]
16
+ return settings
17
17
  } catch (error) {
18
18
  console.error('Error fetching all form settings:', error)
19
19
  return []
@@ -43,7 +43,7 @@ export async function createMedia(data: CreateMediaInput): Promise<CreateMediaRe
43
43
  user: actor,
44
44
  timestamp: now,
45
45
  sourceType: 'admin_ui' as const,
46
- changedFields: getTouchedFieldNames(data as unknown as Record<string, unknown>),
46
+ changedFields: getTouchedFieldNames(data),
47
47
  metadata: { contentType: media.contentType, size: media.size },
48
48
  ...auditRequestMetadata
49
49
  })
@@ -52,6 +52,7 @@ export async function createMedia(data: CreateMediaInput): Promise<CreateMediaRe
52
52
  ? { success: true, media }
53
53
  : { success: false, error: 'Failed to create media record' }
54
54
  } catch (error) {
55
+ console.error('Failed to create media record:', error)
55
56
  return {
56
57
  success: false,
57
58
  error: toActionError(error, 'Failed to create media record')
@@ -45,6 +45,7 @@ export async function deleteMediaBulk(ids: string[]): Promise<DeleteMediaBulkRes
45
45
  }
46
46
  return { success: true, deletedCount: deletedRows.length }
47
47
  } catch (error) {
48
+ console.error('Failed to delete media:', error)
48
49
  return {
49
50
  success: false,
50
51
  error: toActionError(error, 'Failed to delete media')
@@ -36,6 +36,7 @@ export async function deleteMedia(id: string): Promise<DeleteMediaResult> {
36
36
  })
37
37
  return { success: true }
38
38
  } catch (error) {
39
+ console.error('Failed to delete media:', error)
39
40
  return {
40
41
  success: false,
41
42
  error: toActionError(error, 'Failed to delete media')
@@ -41,15 +41,13 @@ export async function updateMedia(
41
41
  user: actor,
42
42
  timestamp: now,
43
43
  sourceType: 'admin_ui' as const,
44
- changedFields: getChangedFieldNames(
45
- previousMedia as unknown as Record<string, unknown>,
46
- data as Record<string, unknown>
47
- ),
44
+ changedFields: getChangedFieldNames(previousMedia, data),
48
45
  metadata: { contentType: previousMedia?.contentType ?? null },
49
46
  ...auditRequestMetadata
50
47
  })
51
48
  return { success: true }
52
49
  } catch (error) {
50
+ console.error('Failed to update media:', error)
53
51
  return {
54
52
  success: false,
55
53
  error: toActionError(error, 'Failed to update media')
@@ -52,6 +52,7 @@ export async function changeOwnPassword(
52
52
 
53
53
  return { success: true }
54
54
  } catch (error) {
55
+ console.error('Failed to change password:', error)
55
56
  return {
56
57
  success: false,
57
58
  error: toActionError(error, 'Failed to change password')
@@ -67,6 +67,7 @@ export async function updateEmail(
67
67
 
68
68
  return { success: true }
69
69
  } catch (error) {
70
+ console.error('Failed to update email:', error)
70
71
  return {
71
72
  success: false,
72
73
  error: toActionError(error, 'Failed to update email')
@@ -90,6 +90,7 @@ export async function uploadFiles(
90
90
 
91
91
  return { success: true, files: uploadedFiles }
92
92
  } catch (error) {
93
+ console.error('Failed to upload files:', error)
93
94
  return { success: false, error: toActionError(error, 'Failed to upload files') }
94
95
  }
95
96
  }
@@ -22,12 +22,21 @@ export async function uploadMediaFromUrl(
22
22
  return { success: false, error: 'Only HTTP and HTTPS URLs are allowed' }
23
23
  }
24
24
 
25
- const response = await fetch(mediaUrl, {
26
- headers: {
27
- 'User-Agent': 'Mozilla/5.0 (compatible; BetterStartMediaFetcher/1.0)'
28
- },
29
- signal: AbortSignal.timeout(REMOTE_FETCH_TIMEOUT_MS)
30
- })
25
+ let response: Response
26
+ try {
27
+ response = await fetch(mediaUrl, {
28
+ headers: {
29
+ 'User-Agent': 'Mozilla/5.0 (compatible; BetterStartMediaFetcher/1.0)'
30
+ },
31
+ signal: AbortSignal.timeout(REMOTE_FETCH_TIMEOUT_MS)
32
+ })
33
+ } catch (error) {
34
+ console.error('Failed to fetch remote media:', error)
35
+ return {
36
+ success: false,
37
+ error: "Couldn't reach that URL. Check the link and try again."
38
+ }
39
+ }
31
40
 
32
41
  if (!response.ok) {
33
42
  return {
@@ -129,6 +138,7 @@ export async function uploadMediaFromUrl(
129
138
  ]
130
139
  }
131
140
  } catch (error) {
141
+ console.error('Failed to upload media from URL:', error)
132
142
  return { success: false, error: toActionError(error, 'Failed to upload media from URL') }
133
143
  }
134
144
  }
@@ -70,6 +70,7 @@ export async function changeUserPassword(
70
70
 
71
71
  return { success: true }
72
72
  } catch (error) {
73
+ console.error('Failed to change user password:', error)
73
74
  return {
74
75
  success: false,
75
76
  error: toActionError(error, 'Failed to change user password')
@@ -48,7 +48,7 @@ export async function createUser(input: CreateUserInput): Promise<CreateUserResu
48
48
  user: actor,
49
49
  timestamp: now,
50
50
  sourceType: 'admin_ui' as const,
51
- changedFields: getTouchedFieldNames(input as unknown as Record<string, unknown>),
51
+ changedFields: getTouchedFieldNames(input),
52
52
  metadata: { role: input.role },
53
53
  ...auditRequestMetadata
54
54
  })
@@ -66,6 +66,7 @@ export async function createUser(input: CreateUserInput): Promise<CreateUserResu
66
66
  }
67
67
  }
68
68
  } catch (error) {
69
+ console.error('Failed to create user:', error)
69
70
  return {
70
71
  success: false,
71
72
  error: toActionError(error, 'Failed to create user')
@@ -39,6 +39,7 @@ export async function deleteUser(userId: string): Promise<DeleteUserResult> {
39
39
  })
40
40
  return { success: true }
41
41
  } catch (error) {
42
+ console.error('Failed to delete user:', error)
42
43
  return {
43
44
  success: false,
44
45
  error: toActionError(error, 'Failed to delete user')
@@ -15,7 +15,7 @@ export interface UsersPage {
15
15
  total: number
16
16
  }
17
17
 
18
- export interface CreateUserInput {
18
+ export type CreateUserInput = {
19
19
  email: string
20
20
  name: string
21
21
  password: string
@@ -43,6 +43,7 @@ export async function updateUserRole(
43
43
 
44
44
  return { success: true }
45
45
  } catch (error) {
46
+ console.error('Failed to update user role:', error)
46
47
  return {
47
48
  success: false,
48
49
  error: toActionError(error, 'Failed to update user role')
@@ -1,3 +1,8 @@
1
+ import type {
2
+ AuditLogAction,
3
+ AuditLogCategory,
4
+ AuditLogSourceType
5
+ } from '@admin/actions/audit/types'
1
6
  import { sql } from 'drizzle-orm'
2
7
  import {
3
8
  boolean,
@@ -193,8 +198,8 @@ export const auditLog = pgTable(
193
198
  'AuditLog',
194
199
  {
195
200
  id: uuid().defaultRandom().primaryKey().notNull(),
196
- category: varchar('category', { length: 50 }).notNull(),
197
- action: varchar('action', { length: 50 }).notNull(),
201
+ category: varchar('category', { length: 50 }).notNull().$type<AuditLogCategory>(),
202
+ action: varchar('action', { length: 50 }).notNull().$type<AuditLogAction>(),
198
203
  targetType: varchar('target_type', { length: 100 }).notNull(),
199
204
  targetId: text('target_id'),
200
205
  targetLabel: text('target_label'),
@@ -204,14 +209,17 @@ export const auditLog = pgTable(
204
209
  actorEmail: text('actor_email'),
205
210
  actorName: text('actor_name'),
206
211
  actorRole: text('actor_role'),
207
- sourceType: varchar('source_type', { length: 50 }).notNull().default('server_action'),
212
+ sourceType: varchar('source_type', { length: 50 })
213
+ .notNull()
214
+ .default('server_action')
215
+ .$type<AuditLogSourceType>(),
208
216
  ipAddress: varchar('ip_address', { length: 100 }),
209
217
  userAgent: varchar('user_agent', { length: 500 }),
210
218
  changedFields: text('changed_fields')
211
219
  .array()
212
220
  .notNull()
213
221
  .default(sql`ARRAY[]::text[]`),
214
- metadata: jsonb('metadata'),
222
+ metadata: jsonb('metadata').$type<Record<string, unknown>>(),
215
223
  createdAt: timestamp('created_at', { precision: 3, mode: 'string' })
216
224
  .default(sql`CURRENT_TIMESTAMP`)
217
225
  .notNull()
@@ -40,11 +40,11 @@ export function registerHook<Event extends LifecycleHookEvent>(
40
40
  }
41
41
  }
42
42
 
43
- export function getRegisteredHooks<Event extends LifecycleHookEvent>(
43
+ export function getRegisteredHooks(
44
44
  entityType: string,
45
- event: Event
46
- ): LifecycleHookHandler<Event>[] {
47
- return [...((lifecycleHooks.get(entityType)?.get(event) ?? []) as LifecycleHookHandler<Event>[])]
45
+ event: LifecycleHookEvent
46
+ ): LifecycleHookHandler<LifecycleHookEvent>[] {
47
+ return [...(lifecycleHooks.get(entityType)?.get(event) ?? [])]
48
48
  }
49
49
 
50
50
  export function getLifecycleHookSummary(): Record<
@@ -5,15 +5,11 @@ import type {
5
5
  LifecycleHookAbort,
6
6
  LifecycleHookContext
7
7
  } from './types'
8
+ import { isRecord } from '@admin/utils/shared/is-record'
8
9
  import { getRegisteredAdminHooks, getRegisteredHooks } from './registry'
9
10
 
10
11
  function isLifecycleHookAbort(value: unknown): value is LifecycleHookAbort {
11
- return (
12
- typeof value === 'object' &&
13
- value !== null &&
14
- 'abort' in value &&
15
- (value as { abort: unknown }).abort === true
16
- )
12
+ return isRecord(value) && value.abort === true
17
13
  }
18
14
 
19
15
  function createAbortError(event: BeforeLifecycleHookEvent, context: LifecycleHookContext): Error {
@@ -98,7 +94,7 @@ function toAdminEventContext(
98
94
  }
99
95
 
100
96
  function runSettledHook<Context>(
101
- handler: (context: Context) => void | Promise<void>,
97
+ handler: (context: Context) => void,
102
98
  context: Context
103
99
  ): Promise<void> {
104
100
  try {
@@ -0,0 +1,3 @@
1
+ export default function ContainerLayout({ children }: { children: React.ReactNode }) {
2
+ return <div className="container mx-auto">{children}</div>
3
+ }