betterstart-cli 0.0.22 → 0.0.23

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 (92) hide show
  1. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-branding-skeleton.tsx +1 -1
  2. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu-skeleton.tsx +1 -1
  3. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu.tsx +1 -1
  4. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +2 -2
  5. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx +1 -1
  6. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-version-item.tsx +1 -1
  7. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-current-row.tsx +1 -1
  8. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +3 -3
  9. package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +2 -2
  10. package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-drawer.tsx +3 -3
  11. package/dist/assets/adapters/next/templates/init/components/shared/media/media-gallery-dialog.tsx +62 -64
  12. package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-item.tsx +1 -1
  13. package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +1 -1
  14. package/dist/assets/adapters/next/templates/init/data/navigation.ts +1 -3
  15. package/dist/assets/adapters/next/templates/init/lib/actions/auth/auth.ts +8 -0
  16. package/dist/assets/adapters/next/templates/init/lib/actions/profile/change-own-password.ts +59 -0
  17. package/dist/assets/adapters/next/templates/init/lib/actions/profile/index.ts +6 -1
  18. package/dist/assets/adapters/next/templates/init/lib/actions/profile/types.ts +10 -0
  19. package/dist/assets/adapters/next/templates/init/lib/actions/users/change-user-password.ts +77 -0
  20. package/dist/assets/adapters/next/templates/init/lib/actions/users/index.ts +3 -0
  21. package/dist/assets/adapters/next/templates/init/lib/actions/users/types.ts +10 -0
  22. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +4 -0
  23. package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +1 -1
  24. package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +1 -1
  25. package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +1 -1
  26. package/dist/assets/adapters/next/templates/init/pages/media/media-page-content.tsx +13 -18
  27. package/dist/assets/adapters/next/templates/init/pages/profile/profile-form.tsx +133 -1
  28. package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
  29. package/dist/assets/adapters/next/templates/init/pages/users/change-password-dialog.tsx +229 -0
  30. package/dist/assets/adapters/next/templates/init/pages/users/columns.tsx +22 -14
  31. package/dist/assets/adapters/next/templates/init/pages/users/create-user-dialog.tsx +99 -93
  32. package/dist/assets/adapters/next/templates/init/pages/users/delete-user-dialog.tsx +17 -25
  33. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +5 -4
  34. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-bubble-menu.tsx +1 -1
  35. package/dist/assets/shared-assets/react-admin/custom/content-editor/remove-table-part-icon.tsx +1 -1
  36. package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +2 -2
  37. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +1 -1
  38. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-button/color-highlight-button.tsx +2 -2
  39. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-content.tsx +1 -1
  40. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +1 -1
  41. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/highlight-color-button.tsx +2 -2
  42. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/source-color-highlight-popover.tsx +1 -1
  43. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +1 -1
  44. package/dist/assets/shared-assets/react-admin/custom/media-field-empty-state.tsx +19 -12
  45. package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +2 -2
  46. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +1 -1
  47. package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
  48. package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
  49. package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
  50. package/dist/assets/shared-assets/react-admin/ui/alert-dialog.tsx +15 -4
  51. package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
  52. package/dist/assets/shared-assets/react-admin/ui/avatar.tsx +5 -5
  53. package/dist/assets/shared-assets/react-admin/ui/badge.tsx +1 -1
  54. package/dist/assets/shared-assets/react-admin/ui/button-group.tsx +3 -3
  55. package/dist/assets/shared-assets/react-admin/ui/button.tsx +6 -6
  56. package/dist/assets/shared-assets/react-admin/ui/card.tsx +4 -4
  57. package/dist/assets/shared-assets/react-admin/ui/carousel.tsx +2 -2
  58. package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
  59. package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +4 -4
  60. package/dist/assets/shared-assets/react-admin/ui/command.tsx +3 -3
  61. package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
  62. package/dist/assets/shared-assets/react-admin/ui/dialog.tsx +29 -6
  63. package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
  64. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +6 -6
  65. package/dist/assets/shared-assets/react-admin/ui/empty.tsx +2 -2
  66. package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
  67. package/dist/assets/shared-assets/react-admin/ui/hover-card.tsx +1 -1
  68. package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +5 -6
  69. package/dist/assets/shared-assets/react-admin/ui/input-otp.tsx +2 -2
  70. package/dist/assets/shared-assets/react-admin/ui/input.tsx +1 -1
  71. package/dist/assets/shared-assets/react-admin/ui/item.tsx +2 -2
  72. package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +8 -8
  73. package/dist/assets/shared-assets/react-admin/ui/native-select.tsx +1 -1
  74. package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +4 -4
  75. package/dist/assets/shared-assets/react-admin/ui/popover.tsx +1 -1
  76. package/dist/assets/shared-assets/react-admin/ui/progress.tsx +1 -1
  77. package/dist/assets/shared-assets/react-admin/ui/radio-group.tsx +2 -2
  78. package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
  79. package/dist/assets/shared-assets/react-admin/ui/scroll-area.tsx +1 -1
  80. package/dist/assets/shared-assets/react-admin/ui/select.tsx +3 -3
  81. package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +10 -10
  82. package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
  83. package/dist/assets/shared-assets/react-admin/ui/slider.tsx +2 -2
  84. package/dist/assets/shared-assets/react-admin/ui/switch.tsx +2 -2
  85. package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +3 -3
  86. package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
  87. package/dist/assets/shared-assets/react-admin/ui/toggle-group.tsx +2 -2
  88. package/dist/assets/shared-assets/react-admin/ui/toggle.tsx +1 -1
  89. package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
  90. package/dist/cli.js +5372 -4243
  91. package/dist/cli.js.map +1 -1
  92. package/package.json +1 -1
@@ -3,7 +3,7 @@ import { Skeleton } from '@admin/components/ui/skeleton'
3
3
  export function BrandingSkeleton() {
4
4
  return (
5
5
  <div className="flex items-center gap-2 w-full h-full">
6
- <Skeleton className="size-8 rounded-full shrink-0" />
6
+ <Skeleton className="size-8 rounded-sm shrink-0" />
7
7
  <Skeleton className="h-4 w-24 group-data-[collapsible=icon]:hidden" />
8
8
  </div>
9
9
  )
@@ -6,7 +6,7 @@ export function UserMenuSkeleton() {
6
6
  <SidebarMenu className="h-full gap-0">
7
7
  <SidebarMenuItem className="h-full">
8
8
  <div className="flex h-full w-full items-center gap-2 px-3">
9
- <Skeleton className="size-8 rounded-full shrink-0" />
9
+ <Skeleton className="size-8 rounded-sm shrink-0" />
10
10
  <Skeleton className="h-4 w-20 group-data-[collapsible=icon]:hidden" />
11
11
  </div>
12
12
  </SidebarMenuItem>
@@ -43,7 +43,7 @@ export async function SidebarUserMenu() {
43
43
  <span
44
44
  className={cn(
45
45
  buttonVariants({ variant: 'outline', size: 'icon' }),
46
- 'rounded-full group-hover:bg-white size-7'
46
+ 'rounded-sm group-hover:bg-white size-7'
47
47
  )}
48
48
  >
49
49
  <Ellipsis strokeWidth={2} className="size-4" />
@@ -53,14 +53,14 @@ export function LifecycleHooksTab({ lifecycleHooks }: LifecycleHooksTabProps) {
53
53
 
54
54
  if (!firstHook) {
55
55
  return (
56
- <div className="flex min-h-80 items-center justify-center rounded-lg border border-dashed border-border px-6 text-center text-sm text-muted-foreground">
56
+ <div className="flex min-h-80 items-center justify-center rounded-sm border border-dashed border-border px-6 text-center text-sm text-muted-foreground">
57
57
  Lifecycle hook examples are generated for entity and form schemas.
58
58
  </div>
59
59
  )
60
60
  }
61
61
 
62
62
  return (
63
- <Accordion type="single" defaultValue={firstHook.id} className="min-h-0 shrink-0 rounded-lg">
63
+ <Accordion type="single" defaultValue={firstHook.id} className="min-h-0 shrink-0 rounded-sm">
64
64
  {lifecycleHooks.map((hook) => {
65
65
  const HookIcon = LIFECYCLE_HOOK_ICONS[hook.icon]
66
66
  const phaseLabel = LIFECYCLE_HOOK_PHASE_LABELS[hook.phase]
@@ -57,7 +57,7 @@ export function SnippetsTab({ snippets }: SnippetsTabProps) {
57
57
  }
58
58
 
59
59
  return (
60
- <Accordion type="single" defaultValue={firstSnippet.id} className="min-h-0 shrink-0 rounded-lg">
60
+ <Accordion type="single" defaultValue={firstSnippet.id} className="min-h-0 shrink-0 rounded-sm">
61
61
  {snippets.map((snippet) => {
62
62
  const SnippetIcon = SNIPPET_ICONS[snippet.icon]
63
63
  const snippetKindLabel = SNIPPET_KIND_LABELS[snippet.kind]
@@ -24,7 +24,7 @@ export function EntityVersionItem({
24
24
  const authorLabel = author?.name || author?.email || 'System'
25
25
 
26
26
  return (
27
- <div className="flex items-center justify-between gap-3 rounded-2xl border border-border px-3 py-3">
27
+ <div className="flex items-center justify-between gap-3 rounded-sm border border-border shadow-input px-3 py-3">
28
28
  <div className="flex min-w-0 items-center gap-3">
29
29
  <Avatar size="sm">
30
30
  {author?.image ? <AvatarImage src={author.image} alt={authorLabel} /> : null}
@@ -22,7 +22,7 @@ export function EntityVersionsCurrentRow({ updatedAt, author }: EntityVersionsCu
22
22
  const authorLabel = author?.name || author?.email || 'System'
23
23
 
24
24
  return (
25
- <div className="flex items-center justify-between gap-3 rounded-2xl border border-border bg-card px-3 py-3">
25
+ <div className="flex items-center justify-between gap-3 rounded-sm border border-border shadow-input bg-card px-3 py-3">
26
26
  <div className="flex min-w-0 items-center gap-3">
27
27
  <Avatar size="sm">
28
28
  {author?.image ? <AvatarImage src={author.image} alt={authorLabel} /> : null}
@@ -51,15 +51,15 @@ export function EntityVersionsDrawer({
51
51
  </DrawerDescription>
52
52
  </DrawerHeader>
53
53
  <ScrollArea className="min-h-0 flex-1">
54
- <div className="flex flex-col gap-3 p-4">
54
+ <div className="flex flex-col gap-1 px-6">
55
55
  <EntityVersionsCurrentRow updatedAt={currentUpdatedAt} author={currentAuthor} />
56
56
  {isPending ? (
57
- <p className="rounded-2xl border border-dashed border-border px-3 py-6 text-center text-sm text-muted-foreground">
57
+ <p className="rounded-sm border border-dashed border-border px-3 py-6 text-center text-sm text-muted-foreground">
58
58
  Loading versions...
59
59
  </p>
60
60
  ) : null}
61
61
  {!isPending && versions.length === 0 ? (
62
- <p className="rounded-2xl border border-dashed border-border px-3 py-6 text-center text-sm text-muted-foreground">
62
+ <p className="rounded-sm border border-dashed border-border px-3 py-6 text-center text-sm text-muted-foreground">
63
63
  No previous versions yet.
64
64
  </p>
65
65
  ) : null}
@@ -93,7 +93,7 @@ export function EditMediaDialogContent({
93
93
  <DrawerContent className="h-full w-full overflow-hidden">
94
94
  <div
95
95
  ref={handleDrawerContainerRef}
96
- className="pointer-events-none absolute inset-3 z-20 overflow-hidden rounded-lg"
96
+ className="pointer-events-none absolute inset-3 z-20 overflow-hidden rounded-sm"
97
97
  />
98
98
  <div className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden gap-3">
99
99
  <DrawerHeader>
@@ -107,7 +107,7 @@ export function EditMediaDialogContent({
107
107
  >
108
108
  <Card>
109
109
  <CardContent className="p-0">
110
- <div className="relative aspect-video overflow-hidden rounded-lg bg-background">
110
+ <div className="relative aspect-video overflow-hidden rounded-sm bg-background">
111
111
  <MediaPreview media={media} showMetadata />
112
112
  </div>
113
113
  </CardContent>
@@ -105,15 +105,15 @@ export function MediaDeleteDrawer({
105
105
  <DrawerPrimitive.Overlay className="pointer-events-auto absolute inset-0 z-20 bg-black/45 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0" />
106
106
  <DrawerPrimitive.Content className="pointer-events-auto absolute inset-x-0 bottom-0 z-30 max-h-[calc(100%-1rem)] overflow-hidden p-3 outline-none">
107
107
  <div
108
- className="overflow-hidden rounded-4xl border border-border bg-popover text-sm text-popover-foreground shadow-2xl transition-[height] duration-200 ease-out"
108
+ className="overflow-hidden rounded-sm border border-border bg-popover text-sm text-popover-foreground shadow-2xl transition-[height] duration-200 ease-out"
109
109
  style={contentHeight ? { height: contentHeight } : undefined}
110
110
  >
111
111
  <div
112
112
  ref={contentRef}
113
113
  className="max-h-[calc(100vh-8rem)] overflow-y-auto px-4 pb-4 pt-4"
114
114
  >
115
- <div aria-hidden="true" className="mx-auto mb-5 h-1.5 w-20 rounded-full bg-muted" />
116
- <div className="mb-3 flex size-10 items-center justify-center rounded-full bg-destructive/10 text-foreground">
115
+ <div aria-hidden="true" className="mx-auto mb-5 h-1.5 w-20 rounded-sm bg-muted" />
116
+ <div className="mb-3 flex size-10 items-center justify-center rounded-sm bg-destructive/10 text-foreground">
117
117
  <TriangleAlert className="size-5" />
118
118
  </div>
119
119
  <DrawerPrimitive.Title className="font-heading text-base font-medium text-foreground">
@@ -4,6 +4,7 @@ import { UploadDropzone } from '@admin/components/custom/upload-dropzone'
4
4
  import { Button } from '@admin/components/ui/button'
5
5
  import {
6
6
  Dialog,
7
+ DialogBody,
7
8
  DialogContent,
8
9
  DialogDescription,
9
10
  DialogFooter,
@@ -98,7 +99,7 @@ export function MediaGalleryDialog({
98
99
  onOpenChange(next)
99
100
  }}
100
101
  >
101
- <DialogContent className="flex max-h-[700px] h-[80svh] min-h-0 flex-col overflow-hidden sm:max-w-3xl gap-4">
102
+ <DialogContent className="flex max-h-[700px] h-[80svh] min-h-0 flex-col overflow-hidden sm:max-w-3xl gap-0">
102
103
  <DialogHeader>
103
104
  <DialogTitle>Select file</DialogTitle>
104
105
  <DialogDescription>
@@ -108,73 +109,70 @@ export function MediaGalleryDialog({
108
109
  </DialogDescription>
109
110
  </DialogHeader>
110
111
 
111
- <TabsUi.Tabs
112
- value={activeTab}
113
- onValueChange={(value) => {
114
- void setQueryParams({ mg_tab: value as 'upload' | 'url' })
115
- }}
116
- className="flex min-h-0 w-full flex-col overflow-hidden"
117
- >
118
- <TabsUi.TabsList className="w-full shrink-0">
119
- <TabsUi.TabsTrigger value="upload">Select</TabsUi.TabsTrigger>
120
- <TabsUi.TabsTrigger value="url">Import from URL</TabsUi.TabsTrigger>
121
- </TabsUi.TabsList>
112
+ <DialogBody className="flex-1 gap-4">
113
+ <TabsUi.Tabs
114
+ value={activeTab}
115
+ onValueChange={(value) => {
116
+ void setQueryParams({ mg_tab: value as 'upload' | 'url' })
117
+ }}
118
+ className="flex min-h-0 w-full flex-col overflow-hidden"
119
+ >
120
+ <TabsUi.TabsList className="w-full shrink-0">
121
+ <TabsUi.TabsTrigger value="upload">Select</TabsUi.TabsTrigger>
122
+ <TabsUi.TabsTrigger value="url">Import from URL</TabsUi.TabsTrigger>
123
+ </TabsUi.TabsList>
122
124
 
123
- <TabsUi.TabsContent value="upload" className="min-h-0 h-full flex-1">
124
- <UploadDropzone
125
- onFilesSelected={upload}
126
- progress={progress}
127
- isUploading={isUploading}
128
- accept={accept}
129
- className="h-[180px]"
130
- />
131
- </TabsUi.TabsContent>
125
+ <TabsUi.TabsContent value="upload" className="min-h-0 h-full flex-1">
126
+ <UploadDropzone
127
+ onFilesSelected={upload}
128
+ progress={progress}
129
+ isUploading={isUploading}
130
+ accept={accept}
131
+ className="h-[180px]"
132
+ />
133
+ </TabsUi.TabsContent>
132
134
 
133
- <TabsUi.TabsContent value="url" className="min-h-0 flex-1">
134
- <MediaUrlImporter
135
- onImportFromUrl={uploadFromUrl}
136
- accept={accept}
137
- className="h-[180px]"
138
- />
139
- </TabsUi.TabsContent>
140
- </TabsUi.Tabs>
135
+ <TabsUi.TabsContent value="url" className="min-h-0 flex-1">
136
+ <MediaUrlImporter
137
+ onImportFromUrl={uploadFromUrl}
138
+ accept={accept}
139
+ className="h-[180px]"
140
+ />
141
+ </TabsUi.TabsContent>
142
+ </TabsUi.Tabs>
141
143
 
142
- <MediaGrid
143
- items={media}
144
- selectedIds={selectedMap}
145
- isSelectMode
146
- onItemClick={handleItemClick}
147
- className="pb-5"
148
- />
144
+ <MediaGrid
145
+ items={media}
146
+ selectedIds={selectedMap}
147
+ isSelectMode
148
+ onItemClick={handleItemClick}
149
+ className="pb-5"
150
+ />
151
+ </DialogBody>
149
152
 
150
- <DialogFooter className="mt-auto">
151
- <div className="flex items-center justify-end gap-3">
152
- <Button variant="outline" onClick={() => onOpenChange(false)}>
153
- Cancel
154
- </Button>
155
- <MediaGridPagination
156
- page={safePage}
157
- pageCount={pageCount}
158
- pageSize={safePageSize}
159
- onPageChange={(value) => {
160
- void setQueryParams({ mg_page: value })
161
- }}
162
- onPageSizeChange={(value) => {
163
- void setQueryParams({ mg_size: value })
164
- }}
165
- className="px-0"
166
- isMinimal={true}
167
- />
168
- <Button
169
- onClick={() => {
170
- onSelect(getSelection())
171
- onOpenChange(false)
172
- }}
173
- disabled={selectionCount === 0}
174
- >
175
- {mode === 'single' ? 'Done' : `Done (${selectionCount})`}
176
- </Button>
177
- </div>
153
+ <DialogFooter showCloseButton>
154
+ <MediaGridPagination
155
+ page={safePage}
156
+ pageCount={pageCount}
157
+ pageSize={safePageSize}
158
+ onPageChange={(value) => {
159
+ void setQueryParams({ mg_page: value })
160
+ }}
161
+ onPageSizeChange={(value) => {
162
+ void setQueryParams({ mg_size: value })
163
+ }}
164
+ className="px-0"
165
+ isMinimal={true}
166
+ />
167
+ <Button
168
+ onClick={() => {
169
+ onSelect(getSelection())
170
+ onOpenChange(false)
171
+ }}
172
+ disabled={selectionCount === 0}
173
+ >
174
+ {mode === 'single' ? 'Done' : `Done (${selectionCount})`}
175
+ </Button>
178
176
  </DialogFooter>
179
177
  </DialogContent>
180
178
  </Dialog>
@@ -42,7 +42,7 @@ export const MediaGridItem = React.memo(function MediaGridItem({
42
42
  <Checkbox
43
43
  aria-label={selected ? `Deselect ${item.filename}` : `Select ${item.filename}`}
44
44
  tabIndex={-1}
45
- className={cn('absolute top-3 right-3 z-10 rounded-full bg-card', {
45
+ className={cn('absolute top-3 right-3 z-10 rounded-sm bg-card', {
46
46
  'ring ring-white': selected
47
47
  })}
48
48
  checked={selected}
@@ -90,7 +90,7 @@ export function MediaUrlImporter({ onImportFromUrl, accept, className }: MediaUr
90
90
  return (
91
91
  <div
92
92
  className={cn(
93
- 'flex items-center justify-center transition-colors rounded-lg border bg-input border-border overflow-hidden h-full flex-col p-5 gap-5',
93
+ 'flex items-center justify-center transition-colors rounded-sm border bg-input border-border overflow-hidden h-full flex-col p-5 gap-5',
94
94
  className
95
95
  )}
96
96
  >
@@ -1,8 +1,6 @@
1
1
  import type { AdminNavigationItem } from '@admin/types/navigation'
2
2
  import { House, ImagePlay } from 'lucide-react'
3
- import { contactNav } from './navigation/contact'
4
3
  import { menusNav } from './navigation/menus'
5
- import { portfolioNav } from './navigation/portfolio'
6
4
  import { postsNav } from './navigation/posts'
7
5
 
8
6
  export type { AdminNavigationItem } from '@admin/types/navigation'
@@ -31,7 +29,7 @@ function comparePositions(a?: number, b?: number): number {
31
29
  return 0
32
30
  }
33
31
 
34
- const combinedNavigation = [...coreNav, ...contactNav, ...menusNav, ...portfolioNav, ...postsNav]
32
+ const combinedNavigation = [...coreNav, ...menusNav, ...postsNav]
35
33
 
36
34
  const dashboard = combinedNavigation.find((item) => item.href === '/admin')
37
35
  const others = combinedNavigation.filter((item) => item.href !== '/admin')
@@ -1,9 +1,11 @@
1
1
  import { sendPasswordResetEmail } from '@admin/actions/email'
2
2
  import db from '@admin/db'
3
3
  import * as schema from '@admin/db/schema'
4
+ import { UserRole } from '@admin/types/auth'
4
5
  import { betterAuth } from 'better-auth'
5
6
  import { drizzleAdapter } from 'better-auth/adapters/drizzle'
6
7
  import { toNextJsHandler } from 'better-auth/next-js'
8
+ import { admin } from 'better-auth/plugins/admin'
7
9
 
8
10
  export { toNextJsHandler }
9
11
 
@@ -42,6 +44,12 @@ export const auth = betterAuth({
42
44
  maxAge: 15 * 60 // 15 minutes — avoids DB round-trip for session validation
43
45
  }
44
46
  },
47
+ plugins: [
48
+ admin({
49
+ defaultRole: UserRole.MEMBER,
50
+ adminRoles: [UserRole.ADMIN]
51
+ })
52
+ ],
45
53
  user: {
46
54
  additionalFields: {
47
55
  role: {
@@ -0,0 +1,59 @@
1
+ 'use server'
2
+
3
+ import { getAuditRequestMetadata } from '@admin/actions/audit/request-metadata'
4
+ import { auth } from '@admin/auth'
5
+ import { getSession } from '@admin/auth/middleware'
6
+ import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
7
+ import { headers } from 'next/headers'
8
+ import { invalidateUsersCache } from './invalidate-users-cache'
9
+ import type { ChangeOwnPasswordInput, ChangeOwnPasswordResult } from './types'
10
+
11
+ export async function changeOwnPassword(
12
+ input: ChangeOwnPasswordInput
13
+ ): Promise<ChangeOwnPasswordResult> {
14
+ try {
15
+ const session = await getSession({ disableCookieCache: true })
16
+ if (!session?.user) {
17
+ return { success: false, error: 'Not authenticated' }
18
+ }
19
+
20
+ const now = new Date().toISOString()
21
+ const auditRequestMetadata = await getAuditRequestMetadata()
22
+
23
+ const requestHeaders = await headers()
24
+
25
+ await auth.api.changePassword({
26
+ body: {
27
+ currentPassword: input.currentPassword,
28
+ newPassword: input.newPassword
29
+ },
30
+ headers: requestHeaders
31
+ })
32
+
33
+ await auth.api.revokeOtherSessions({
34
+ headers: requestHeaders
35
+ })
36
+
37
+ await invalidateUsersCache()
38
+ runAdminEventHooks({
39
+ category: 'auth',
40
+ action: 'update',
41
+ targetType: 'user',
42
+ targetId: session.user.id,
43
+ targetLabel: session.user.email,
44
+ user: session.user,
45
+ timestamp: now,
46
+ sourceType: 'admin_ui' as const,
47
+ changedFields: ['password'],
48
+ metadata: { operation: 'password_change' },
49
+ ...auditRequestMetadata
50
+ })
51
+
52
+ return { success: true }
53
+ } catch (error) {
54
+ return {
55
+ success: false,
56
+ error: error instanceof Error ? error.message : 'Failed to change password'
57
+ }
58
+ }
59
+ }
@@ -1,4 +1,9 @@
1
+ export { changeOwnPassword } from './change-own-password'
1
2
  export { invalidateUsersCache } from './invalidate-users-cache'
2
3
  export { isEmailConfigured } from './is-email-configured'
3
- export type { UpdateEmailResult } from './types'
4
+ export type {
5
+ ChangeOwnPasswordInput,
6
+ ChangeOwnPasswordResult,
7
+ UpdateEmailResult
8
+ } from './types'
4
9
  export { updateEmail } from './update-email'
@@ -2,3 +2,13 @@ export interface UpdateEmailResult {
2
2
  success: boolean
3
3
  error?: string
4
4
  }
5
+
6
+ export interface ChangeOwnPasswordInput {
7
+ currentPassword: string
8
+ newPassword: string
9
+ }
10
+
11
+ export interface ChangeOwnPasswordResult {
12
+ success: boolean
13
+ error?: string
14
+ }
@@ -0,0 +1,77 @@
1
+ 'use server'
2
+
3
+ import { getAuditRequestMetadata } from '@admin/actions/audit/request-metadata'
4
+ import { auth } from '@admin/auth'
5
+ import { requireRole, UserRole } from '@admin/auth/middleware'
6
+ import db from '@admin/db'
7
+ import { user } from '@admin/db/schema'
8
+ import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
9
+ import { eq } from 'drizzle-orm'
10
+ import { updateTag } from 'next/cache'
11
+ import { headers } from 'next/headers'
12
+ import type { ChangeUserPasswordInput, ChangeUserPasswordResult } from './types'
13
+ import { usersCacheTags } from './types'
14
+
15
+ export async function changeUserPassword(
16
+ input: ChangeUserPasswordInput
17
+ ): Promise<ChangeUserPasswordResult> {
18
+ const actor = await requireRole([UserRole.ADMIN])
19
+
20
+ try {
21
+ const [targetUser] = await db
22
+ .select({ id: user.id, email: user.email, name: user.name })
23
+ .from(user)
24
+ .where(eq(user.id, input.userId))
25
+ .limit(1)
26
+
27
+ if (!targetUser) {
28
+ return { success: false, error: 'User not found' }
29
+ }
30
+
31
+ if (targetUser.id === actor.id) {
32
+ return {
33
+ success: false,
34
+ error: 'Use your profile to change your own password'
35
+ }
36
+ }
37
+
38
+ const now = new Date().toISOString()
39
+ const requestHeaders = await headers()
40
+ const auditRequestMetadata = await getAuditRequestMetadata()
41
+
42
+ await auth.api.setUserPassword({
43
+ body: {
44
+ userId: input.userId,
45
+ newPassword: input.newPassword
46
+ },
47
+ headers: requestHeaders
48
+ })
49
+
50
+ await auth.api.revokeUserSessions({
51
+ body: { userId: input.userId },
52
+ headers: requestHeaders
53
+ })
54
+
55
+ updateTag(usersCacheTags.all)
56
+ runAdminEventHooks({
57
+ category: 'user',
58
+ action: 'update',
59
+ targetType: 'user',
60
+ targetId: targetUser.id,
61
+ targetLabel: targetUser.email,
62
+ user: actor,
63
+ timestamp: now,
64
+ sourceType: 'admin_ui' as const,
65
+ changedFields: ['password'],
66
+ metadata: { operation: 'password_change' },
67
+ ...auditRequestMetadata
68
+ })
69
+
70
+ return { success: true }
71
+ } catch (error) {
72
+ return {
73
+ success: false,
74
+ error: error instanceof Error ? error.message : 'Failed to change user password'
75
+ }
76
+ }
77
+ }
@@ -1,7 +1,10 @@
1
+ export { changeUserPassword } from './change-user-password'
1
2
  export { createUser } from './create-user'
2
3
  export { deleteUser } from './delete-user'
3
4
  export { getUsers } from './get-users'
4
5
  export type {
6
+ ChangeUserPasswordInput,
7
+ ChangeUserPasswordResult,
5
8
  CreateUserInput,
6
9
  CreateUserResult,
7
10
  DeleteUserResult,
@@ -33,6 +33,16 @@ export interface UpdateUserRoleResult {
33
33
  error?: string
34
34
  }
35
35
 
36
+ export interface ChangeUserPasswordInput {
37
+ userId: string
38
+ newPassword: string
39
+ }
40
+
41
+ export interface ChangeUserPasswordResult {
42
+ success: boolean
43
+ error?: string
44
+ }
45
+
36
46
  export interface DeleteUserResult {
37
47
  success: boolean
38
48
  error?: string
@@ -23,6 +23,9 @@ export const user = pgTable('user', {
23
23
  emailVerified: boolean('email_verified').notNull().default(false),
24
24
  image: text('image'),
25
25
  role: text('role').notNull().default('member'),
26
+ banned: boolean('banned').default(false),
27
+ banReason: text('ban_reason'),
28
+ banExpires: timestamp('ban_expires'),
26
29
  createdAt: timestamp('created_at').notNull().defaultNow(),
27
30
  updatedAt: timestamp('updated_at').notNull().defaultNow()
28
31
  })
@@ -35,6 +38,7 @@ export const session = pgTable('session', {
35
38
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
36
39
  ipAddress: text('ip_address'),
37
40
  userAgent: text('user_agent'),
41
+ impersonatedBy: text('impersonated_by'),
38
42
  userId: text('user_id')
39
43
  .notNull()
40
44
  .references(() => user.id, { onDelete: 'cascade' })
@@ -63,7 +63,7 @@ export default function DashboardPage() {
63
63
  <div className="flex flex-col w-full gap-1">
64
64
  {envVariables.map((env) => (
65
65
  <div
66
- className="flex flex-col w-full bg-card border shadow-card border-border rounded-lg pl-4 pr-2 py-2"
66
+ className="flex flex-col w-full bg-card border shadow-card border-border rounded-sm pl-4 pr-2 py-2"
67
67
  key={env.name}
68
68
  >
69
69
  <div className="flex items-center justify-between">
@@ -1,6 +1,6 @@
1
1
  export function ForgotPasswordPageSkeleton() {
2
2
  return (
3
- <div className="overflow-hidden rounded-lg border bg-card">
3
+ <div className="overflow-hidden rounded-sm border bg-card">
4
4
  <div className="grid md:grid-cols-[5fr_7fr]">
5
5
  <div className="hidden min-h-[460px] bg-muted md:block" />
6
6
  <div className="flex min-h-[460px] flex-col gap-10 p-6 md:p-20">
@@ -1,6 +1,6 @@
1
1
  export function LoginPageSkeleton() {
2
2
  return (
3
- <div className="overflow-hidden rounded-lg border bg-card">
3
+ <div className="overflow-hidden rounded-sm border bg-card">
4
4
  <div className="grid md:grid-cols-[5fr_7fr]">
5
5
  <div className="hidden min-h-[540px] bg-muted md:block" />
6
6
  <div className="flex min-h-[540px] flex-col gap-10 p-6 md:p-20">
@@ -11,8 +11,7 @@ import { MediaGrid } from '@admin/components/shared/media/media-grid'
11
11
  import { MediaGridPagination } from '@admin/components/shared/media/media-grid-pagination'
12
12
  import { MediaUrlImporter } from '@admin/components/shared/media/media-url-importer'
13
13
  import { PageHeader } from '@admin/components/shared/page-header'
14
- import { Button } from '@admin/components/ui/button'
15
- import { ButtonGroup } from '@admin/components/ui/button-group'
14
+ import { Tabs, TabsList, TabsTrigger } from '@admin/components/ui/tabs'
16
15
  import * as mediaHooks from '@admin/hooks/use-media'
17
16
  import { useMediaFiltersBar } from '@admin/hooks/use-media-filters-bar'
18
17
  import { useUpload } from '@admin/hooks/use-upload'
@@ -172,22 +171,18 @@ export function MediaPageContent() {
172
171
  <PageHeader
173
172
  title="Media Gallery"
174
173
  actions={
175
- <ButtonGroup>
176
- <Button
177
- variant={activeTab === 'url' ? 'default' : 'outline'}
178
- onClick={() => setActiveTab('url')}
179
- >
180
- <Link />
181
- Import by URL
182
- </Button>
183
- <Button
184
- variant={activeTab === 'upload' ? 'default' : 'outline'}
185
- onClick={() => setActiveTab('upload')}
186
- >
187
- <Upload />
188
- Upload
189
- </Button>
190
- </ButtonGroup>
174
+ <Tabs defaultValue="upload">
175
+ <TabsList>
176
+ <TabsTrigger value="url" onClick={() => setActiveTab('url')}>
177
+ <Link className="size-3" />
178
+ Import from URL
179
+ </TabsTrigger>
180
+ <TabsTrigger value="upload" onClick={() => setActiveTab('upload')}>
181
+ <Upload className="size-3" />
182
+ Upload
183
+ </TabsTrigger>
184
+ </TabsList>
185
+ </Tabs>
191
186
  }
192
187
  />
193
188