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.
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-branding-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu.tsx +1 -1
- 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/entity-versions/entity-version-item.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-current-row.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +3 -3
- 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-delete-drawer.tsx +3 -3
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-gallery-dialog.tsx +62 -64
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-item.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/data/navigation.ts +1 -3
- package/dist/assets/adapters/next/templates/init/lib/actions/auth/auth.ts +8 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/change-own-password.ts +59 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/index.ts +6 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/types.ts +10 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/users/change-user-password.ts +77 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/users/index.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/users/types.ts +10 -0
- package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +4 -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/media/media-page-content.tsx +13 -18
- package/dist/assets/adapters/next/templates/init/pages/profile/profile-form.tsx +133 -1
- package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/users/change-password-dialog.tsx +229 -0
- package/dist/assets/adapters/next/templates/init/pages/users/columns.tsx +22 -14
- package/dist/assets/adapters/next/templates/init/pages/users/create-user-dialog.tsx +99 -93
- package/dist/assets/adapters/next/templates/init/pages/users/delete-user-dialog.tsx +17 -25
- package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +5 -4
- 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/remove-table-part-icon.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +2 -2
- 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/tiptap-ui/color-highlight-button/color-highlight-button.tsx +2 -2
- 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 +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/highlight-color-button.tsx +2 -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/date-range-picker.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/media-field-empty-state.tsx +19 -12
- package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +1 -1
- 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/ui/accordion.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/alert-dialog.tsx +15 -4
- package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/avatar.tsx +5 -5
- package/dist/assets/shared-assets/react-admin/ui/badge.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/button-group.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/button.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/card.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/carousel.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/command.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/dialog.tsx +29 -6
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +6 -6
- 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 +5 -6
- package/dist/assets/shared-assets/react-admin/ui/input-otp.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/input.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 +8 -8
- package/dist/assets/shared-assets/react-admin/ui/native-select.tsx +1 -1
- 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/progress.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/radio-group.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/scroll-area.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/select.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +10 -10
- package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/slider.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/switch.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/toggle-group.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/toggle.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
- package/dist/cli.js +5372 -4243
- package/dist/cli.js.map +1 -1
- 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-
|
|
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-
|
|
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>
|
package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu.tsx
CHANGED
|
@@ -43,7 +43,7 @@ export async function SidebarUserMenu() {
|
|
|
43
43
|
<span
|
|
44
44
|
className={cn(
|
|
45
45
|
buttonVariants({ variant: 'outline', size: 'icon' }),
|
|
46
|
-
'rounded-
|
|
46
|
+
'rounded-sm group-hover:bg-white size-7'
|
|
47
47
|
)}
|
|
48
48
|
>
|
|
49
49
|
<Ellipsis strokeWidth={2} className="size-4" />
|
package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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]
|
package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
54
|
+
<div className="flex flex-col gap-1 px-6">
|
|
55
55
|
<EntityVersionsCurrentRow updatedAt={currentUpdatedAt} author={currentAuthor} />
|
|
56
56
|
{isPending ? (
|
|
57
|
-
<p className="rounded-
|
|
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-
|
|
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-
|
|
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-
|
|
110
|
+
<div className="relative aspect-video overflow-hidden rounded-sm bg-background">
|
|
111
111
|
<MediaPreview media={media} showMetadata />
|
|
112
112
|
</div>
|
|
113
113
|
</CardContent>
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-drawer.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
116
|
-
<div className="mb-3 flex size-10 items-center justify-center rounded-
|
|
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">
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-gallery-dialog.tsx
CHANGED
|
@@ -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-
|
|
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
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<TabsUi.
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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>
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-item.tsx
CHANGED
|
@@ -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-
|
|
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}
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx
CHANGED
|
@@ -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-
|
|
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, ...
|
|
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 {
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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
|
-
<
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|