@ekrist1/vulse 0.1.6-alpha.3 → 0.1.7-alpha.4
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/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +3 -4
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +11 -10
- package/dist/core/blueprints/compile.d.ts +1 -1
- package/dist/core/blueprints/compile.d.ts.map +1 -1
- package/dist/core/blueprints/compile.js +3 -3
- package/dist/core/blueprints/mutations.js +2 -2
- package/dist/core/forms/rate-limit.d.ts +1 -1
- package/dist/core/forms/rate-limit.d.ts.map +1 -1
- package/dist/core/forms/rate-limit.js +3 -3
- package/dist/core/forms/unique.d.ts +1 -1
- package/dist/core/forms/unique.d.ts.map +1 -1
- package/dist/core/forms/unique.js +4 -4
- package/dist/core/globals/compile.d.ts +1 -1
- package/dist/core/globals/compile.d.ts.map +1 -1
- package/dist/core/globals/compile.js +2 -2
- package/dist/core/globals/definition.d.ts +1 -1
- package/dist/core/globals/definition.d.ts.map +1 -1
- package/dist/core/globals/definition.js +3 -3
- package/dist/core/repos/globals.js +3 -3
- package/dist/core/sha256.d.ts +3 -0
- package/dist/core/sha256.d.ts.map +1 -0
- package/dist/core/sha256.js +9 -0
- package/dist/integration/index.js +1 -1
- package/dist/integration/install-hook.d.ts.map +1 -1
- package/dist/integration/install-hook.js +6 -4
- package/dist/integration/wrangler-config.d.ts +12 -0
- package/dist/integration/wrangler-config.d.ts.map +1 -0
- package/dist/integration/wrangler-config.js +97 -0
- package/dist/integration/wrangler-patch.d.ts +1 -0
- package/dist/integration/wrangler-patch.d.ts.map +1 -1
- package/dist/integration/wrangler-patch.js +2 -1
- package/dist/server/routes/form-submit.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -3
- package/src/admin/assets/logo-mark.svg +5 -0
- package/src/admin/client/active-locale.ts +17 -0
- package/src/admin/client/api.ts +21 -0
- package/src/admin/client/form-from-zod.ts +7 -0
- package/src/admin/client/live-preview-enabled.ts +5 -0
- package/src/admin/components/AdminShell.astro +45 -0
- package/src/admin/components/AuthSettings.vue +60 -0
- package/src/admin/components/BlockEditor.vue +53 -0
- package/src/admin/components/BlueprintEditor.vue +1783 -0
- package/src/admin/components/CollectionKindIcon.vue +26 -0
- package/src/admin/components/CollectionTree.vue +220 -0
- package/src/admin/components/EntryEditorWithPreview.vue +130 -0
- package/src/admin/components/EntryForm.vue +411 -0
- package/src/admin/components/EntryList.vue +121 -0
- package/src/admin/components/EntryStatusBadge.vue +24 -0
- package/src/admin/components/FormEditor.vue +233 -0
- package/src/admin/components/FormList.vue +54 -0
- package/src/admin/components/GlobalSetEditor.vue +272 -0
- package/src/admin/components/GlobalSetList.vue +55 -0
- package/src/admin/components/LivePreviewPanel.vue +171 -0
- package/src/admin/components/LoginForm.vue +53 -0
- package/src/admin/components/MediaLibrary.vue +106 -0
- package/src/admin/components/MediaPicker.vue +49 -0
- package/src/admin/components/RevisionDiff.vue +11 -0
- package/src/admin/components/RevisionList.vue +134 -0
- package/src/admin/components/SeoFields.vue +113 -0
- package/src/admin/components/SetEditor.vue +137 -0
- package/src/admin/components/SetList.vue +32 -0
- package/src/admin/components/SettingsForm.vue +189 -0
- package/src/admin/components/SideNav.vue +152 -0
- package/src/admin/components/SubmissionDetail.vue +45 -0
- package/src/admin/components/SubmissionList.vue +89 -0
- package/src/admin/components/ToastHost.vue +33 -0
- package/src/admin/components/TreeRow.vue +163 -0
- package/src/admin/components/UserEditor.vue +186 -0
- package/src/admin/components/UserList.vue +46 -0
- package/src/admin/components/blocks/BlockItem.vue +32 -0
- package/src/admin/components/blocks/BlockToolbar.vue +12 -0
- package/src/admin/components/blocks/edit/CodeEdit.vue +18 -0
- package/src/admin/components/blocks/edit/EmbedEdit.vue +14 -0
- package/src/admin/components/blocks/edit/HeadingEdit.vue +19 -0
- package/src/admin/components/blocks/edit/ImageEdit.vue +40 -0
- package/src/admin/components/blocks/edit/ListEdit.vue +36 -0
- package/src/admin/components/blocks/edit/ParagraphEdit.vue +14 -0
- package/src/admin/components/blocks/edit/QuoteEdit.vue +18 -0
- package/src/admin/components/fields/BlocksField.vue +123 -0
- package/src/admin/components/fields/BlocksSetsPicker.vue +59 -0
- package/src/admin/components/fields/BoolField.vue +10 -0
- package/src/admin/components/fields/DateField.vue +22 -0
- package/src/admin/components/fields/EntriesField.vue +153 -0
- package/src/admin/components/fields/EntryField.vue +138 -0
- package/src/admin/components/fields/EnumField.vue +81 -0
- package/src/admin/components/fields/FieldRenderer.vue +87 -0
- package/src/admin/components/fields/GridField.vue +173 -0
- package/src/admin/components/fields/LinkField.vue +219 -0
- package/src/admin/components/fields/MediaField.vue +69 -0
- package/src/admin/components/fields/NumberField.vue +12 -0
- package/src/admin/components/fields/ObjectField.vue +18 -0
- package/src/admin/components/fields/RefField.vue +170 -0
- package/src/admin/components/fields/RepeaterField.vue +27 -0
- package/src/admin/components/fields/ReplicatorField.vue +121 -0
- package/src/admin/components/fields/TextField.vue +11 -0
- package/src/admin/components/fields/TextareaField.vue +11 -0
- package/src/admin/components/fields/VulseAccordionGroupNodeView.vue +82 -0
- package/src/admin/components/fields/VulseAccordionNodeView.vue +128 -0
- package/src/admin/components/fields/VulseCalloutNodeView.vue +81 -0
- package/src/admin/components/fields/VulseIframeNodeView.vue +112 -0
- package/src/admin/components/fields/VulseSetNodeView.vue +68 -0
- package/src/admin/components/fields/VulseVideoNodeView.vue +104 -0
- package/src/admin/components/fields/blocks-editor-extensions.ts +26 -0
- package/src/admin/components/fields/emoji-extension.ts +54 -0
- package/src/admin/components/fields/link-extension.ts +48 -0
- package/src/admin/components/fields/set-node-utils.ts +115 -0
- package/src/admin/components/fields/url-utils.ts +85 -0
- package/src/admin/components/fields/vulse-accordion-extension.ts +64 -0
- package/src/admin/components/fields/vulse-accordion-group-extension.ts +49 -0
- package/src/admin/components/fields/vulse-callout-extension.ts +53 -0
- package/src/admin/components/fields/vulse-iframe-extension.ts +96 -0
- package/src/admin/components/fields/vulse-set-extension.ts +66 -0
- package/src/admin/components/fields/vulse-video-extension.ts +65 -0
- package/src/admin/composables/toast.ts +35 -0
- package/src/admin/composables/useEntrySearch.ts +112 -0
- package/src/admin/composables/useSets.ts +31 -0
- package/src/admin/pages/collections/[name]/[id]/revisions.astro +27 -0
- package/src/admin/pages/collections/[name]/[id].astro +90 -0
- package/src/admin/pages/collections/[name]/index.astro +31 -0
- package/src/admin/pages/collections/[name]/new.astro +38 -0
- package/src/admin/pages/forms/[handle]/submissions/[id].astro +9 -0
- package/src/admin/pages/forms/[handle]/submissions/index.astro +9 -0
- package/src/admin/pages/forms/[handle].astro +9 -0
- package/src/admin/pages/forms/index.astro +7 -0
- package/src/admin/pages/forms/new.astro +7 -0
- package/src/admin/pages/index.astro +36 -0
- package/src/admin/pages/login.astro +14 -0
- package/src/admin/pages/media.astro +8 -0
- package/src/admin/pages/schema/[handle].astro +10 -0
- package/src/admin/pages/schema/new.astro +9 -0
- package/src/admin/pages/settings/auth.astro +9 -0
- package/src/admin/pages/settings/globals/[handle].astro +9 -0
- package/src/admin/pages/settings/globals/index.astro +7 -0
- package/src/admin/pages/settings/globals/new.astro +7 -0
- package/src/admin/pages/settings/index.astro +8 -0
- package/src/admin/pages/settings/sets/[handle].astro +9 -0
- package/src/admin/pages/settings/sets/index.astro +7 -0
- package/src/admin/pages/settings/sets/new.astro +7 -0
- package/src/admin/pages/users/[id].astro +10 -0
- package/src/admin/pages/users/index.astro +8 -0
- package/src/admin/styles/admin.css +166 -0
- package/src/core/access.ts +9 -0
- package/src/core/blocks/schema.ts +66 -0
- package/src/core/blueprints/code-to-definition.ts +156 -0
- package/src/core/blueprints/compile.ts +176 -0
- package/src/core/blueprints/define.ts +12 -0
- package/src/core/blueprints/definition.ts +185 -0
- package/src/core/blueprints/load.ts +144 -0
- package/src/core/blueprints/mutations.ts +236 -0
- package/src/core/blueprints/preview-path.ts +33 -0
- package/src/core/blueprints/reflect-fields.ts +305 -0
- package/src/core/blueprints/registry.ts +14 -0
- package/src/core/blueprints/seed.ts +20 -0
- package/src/core/blueprints/select-helpers.ts +30 -0
- package/src/core/blueprints/seo.ts +180 -0
- package/src/core/blueprints/types.ts +59 -0
- package/src/core/blueprints/zod-helpers.ts +86 -0
- package/src/core/db.ts +11 -0
- package/src/core/errors.ts +34 -0
- package/src/core/forms/compile.ts +84 -0
- package/src/core/forms/definition.ts +102 -0
- package/src/core/forms/rate-limit.ts +52 -0
- package/src/core/forms/unique.ts +38 -0
- package/src/core/globals/compile.ts +35 -0
- package/src/core/globals/definition.ts +27 -0
- package/src/core/locales.ts +45 -0
- package/src/core/migrations.ts +48 -0
- package/src/core/parse-content.ts +85 -0
- package/src/core/plugins/definition.ts +150 -0
- package/src/core/preview-content.ts +21 -0
- package/src/core/repos/entries.ts +504 -0
- package/src/core/repos/forms.ts +270 -0
- package/src/core/repos/globals.ts +179 -0
- package/src/core/repos/media.ts +106 -0
- package/src/core/repos/preview-sessions.ts +108 -0
- package/src/core/repos/revisions.ts +60 -0
- package/src/core/repos/settings.ts +23 -0
- package/src/core/schema.ts +244 -0
- package/src/core/sets/compile.ts +12 -0
- package/src/core/sets/definition.ts +10 -0
- package/src/core/sets/service.ts +82 -0
- package/src/core/sets/validate-tree.ts +57 -0
- package/src/core/sha256.ts +10 -0
- package/src/core/slug.ts +30 -0
- package/src/scaffold/collection-write.ts +83 -0
- package/src/scaffold/collection.ts +277 -0
- package/src/server/assets/live-preview-bridge.content.ts +2 -0
- package/src/server/assets/live-preview-bridge.js +535 -0
- package/src/server/better-auth.ts +82 -0
- package/src/server/cf-images.ts +34 -0
- package/src/server/cron.ts +37 -0
- package/src/server/email.ts +17 -0
- package/src/server/endpoints/api-auth.ts +10 -0
- package/src/server/endpoints/api-vulse-blueprints.ts +23 -0
- package/src/server/endpoints/api-vulse-entries-locales.ts +12 -0
- package/src/server/endpoints/api-vulse-entries-move.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-publish.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-tree.ts +7 -0
- package/src/server/endpoints/api-vulse-entries.ts +23 -0
- package/src/server/endpoints/api-vulse-form-handle.ts +30 -0
- package/src/server/endpoints/api-vulse-form-public.ts +7 -0
- package/src/server/endpoints/api-vulse-form-submit.ts +7 -0
- package/src/server/endpoints/api-vulse-form-upload.ts +7 -0
- package/src/server/endpoints/api-vulse-forms.ts +12 -0
- package/src/server/endpoints/api-vulse-globals-handle.ts +20 -0
- package/src/server/endpoints/api-vulse-globals-public-handle.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-public.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-value.ts +8 -0
- package/src/server/endpoints/api-vulse-globals.ts +12 -0
- package/src/server/endpoints/api-vulse-media-file.ts +7 -0
- package/src/server/endpoints/api-vulse-media-id.ts +12 -0
- package/src/server/endpoints/api-vulse-media.ts +12 -0
- package/src/server/endpoints/api-vulse-preview-bridge.ts +11 -0
- package/src/server/endpoints/api-vulse-preview-sessions-id.ts +10 -0
- package/src/server/endpoints/api-vulse-preview-sessions.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-start.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-stop.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions-restore.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions.ts +7 -0
- package/src/server/endpoints/api-vulse-search.ts +7 -0
- package/src/server/endpoints/api-vulse-sets.ts +23 -0
- package/src/server/endpoints/api-vulse-settings.ts +12 -0
- package/src/server/endpoints/api-vulse-users-id.ts +12 -0
- package/src/server/endpoints/api-vulse-users-reset-password.ts +7 -0
- package/src/server/endpoints/api-vulse-users-role.ts +9 -0
- package/src/server/endpoints/api-vulse-users.ts +7 -0
- package/src/server/endpoints/with-runtime.ts +11 -0
- package/src/server/env.ts +23 -0
- package/src/server/envelope.ts +21 -0
- package/src/server/forms/email.ts +11 -0
- package/src/server/forms/process-submission.ts +95 -0
- package/src/server/forms/queue.ts +25 -0
- package/src/server/forms/templates.ts +24 -0
- package/src/server/forms/webhook.ts +19 -0
- package/src/server/handler.ts +66 -0
- package/src/server/image-probe.ts +35 -0
- package/src/server/loader.ts +54 -0
- package/src/server/plugins.ts +214 -0
- package/src/server/preview.ts +25 -0
- package/src/server/r2.ts +13 -0
- package/src/server/routes/blueprints.ts +62 -0
- package/src/server/routes/entries.ts +255 -0
- package/src/server/routes/form-submit.ts +168 -0
- package/src/server/routes/form-upload.ts +100 -0
- package/src/server/routes/forms.ts +88 -0
- package/src/server/routes/globals-public.ts +30 -0
- package/src/server/routes/globals.ts +93 -0
- package/src/server/routes/media.ts +145 -0
- package/src/server/routes/preview-sessions.ts +76 -0
- package/src/server/routes/preview.ts +36 -0
- package/src/server/routes/revisions.ts +29 -0
- package/src/server/routes/search.ts +31 -0
- package/src/server/routes/sets.ts +40 -0
- package/src/server/routes/settings.ts +24 -0
- package/src/server/routes/users.ts +127 -0
- package/src/server/runtime.ts +99 -0
- package/src/server/sdk/collections.ts +98 -0
- package/src/server/sdk/index.ts +25 -0
- package/src/server/sdk/media.ts +11 -0
- package/src/server/sdk/search.ts +90 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import type { Auth } from '../better-auth.js'
|
|
3
|
+
import type { VulseDb } from '../../core/db.js'
|
|
4
|
+
import { GlobalsRepo } from '../../core/repos/globals.js'
|
|
5
|
+
import { GlobalSetDefinitionSchema } from '../../core/globals/definition.js'
|
|
6
|
+
import { NotFoundError } from '../../core/errors.js'
|
|
7
|
+
import { defineHandler } from '../handler.js'
|
|
8
|
+
|
|
9
|
+
const paramsHandle = z.object({ handle: z.string() })
|
|
10
|
+
|
|
11
|
+
export function globalsRoutes(db: VulseDb, auth: Auth) {
|
|
12
|
+
const globals = new GlobalsRepo(db)
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
list: defineHandler(auth, { requireRole: ['admin', 'editor'] }, async () => {
|
|
16
|
+
const rows = await globals.listSets()
|
|
17
|
+
return rows.map((row) => ({
|
|
18
|
+
handle: row.handle,
|
|
19
|
+
label: row.label,
|
|
20
|
+
fieldCount: row.definition.fields.length,
|
|
21
|
+
createdAt: row.createdAt,
|
|
22
|
+
updatedAt: row.updatedAt,
|
|
23
|
+
}))
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
get: defineHandler(auth, {
|
|
27
|
+
params: paramsHandle,
|
|
28
|
+
requireRole: ['admin', 'editor'],
|
|
29
|
+
}, async ({ params }) => {
|
|
30
|
+
const set = await globals.findSetByHandle(params.handle)
|
|
31
|
+
if (!set) throw new NotFoundError('global set not found')
|
|
32
|
+
const value = await globals.getValue(params.handle)
|
|
33
|
+
return {
|
|
34
|
+
set: {
|
|
35
|
+
handle: set.handle,
|
|
36
|
+
label: set.label,
|
|
37
|
+
fields: set.definition.fields,
|
|
38
|
+
createdAt: set.createdAt,
|
|
39
|
+
updatedAt: set.updatedAt,
|
|
40
|
+
},
|
|
41
|
+
value: value ? {
|
|
42
|
+
handle: value.handle,
|
|
43
|
+
content: value.content,
|
|
44
|
+
createdAt: value.createdAt,
|
|
45
|
+
updatedAt: value.updatedAt,
|
|
46
|
+
} : null,
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
create: defineHandler(auth, {
|
|
51
|
+
body: GlobalSetDefinitionSchema,
|
|
52
|
+
requireRole: ['admin'],
|
|
53
|
+
}, async ({ body }) => {
|
|
54
|
+
const row = await globals.createSet(body)
|
|
55
|
+
return {
|
|
56
|
+
handle: row.handle,
|
|
57
|
+
label: row.label,
|
|
58
|
+
fields: row.definition.fields,
|
|
59
|
+
createdAt: row.createdAt,
|
|
60
|
+
updatedAt: row.updatedAt,
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
update: defineHandler(auth, {
|
|
65
|
+
params: paramsHandle,
|
|
66
|
+
body: GlobalSetDefinitionSchema,
|
|
67
|
+
requireRole: ['admin'],
|
|
68
|
+
}, async ({ params, body }) => {
|
|
69
|
+
const row = await globals.updateSet(params.handle, body)
|
|
70
|
+
return {
|
|
71
|
+
handle: row.handle,
|
|
72
|
+
label: row.label,
|
|
73
|
+
fields: row.definition.fields,
|
|
74
|
+
createdAt: row.createdAt,
|
|
75
|
+
updatedAt: row.updatedAt,
|
|
76
|
+
}
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
updateValue: defineHandler(auth, {
|
|
80
|
+
params: paramsHandle,
|
|
81
|
+
body: z.record(z.string(), z.unknown()),
|
|
82
|
+
requireRole: ['admin'],
|
|
83
|
+
}, async ({ params, body }) => globals.updateValue(params.handle, body)),
|
|
84
|
+
|
|
85
|
+
delete: defineHandler(auth, {
|
|
86
|
+
params: paramsHandle,
|
|
87
|
+
requireRole: ['admin'],
|
|
88
|
+
}, async ({ params }) => {
|
|
89
|
+
await globals.deleteSet(params.handle)
|
|
90
|
+
return null
|
|
91
|
+
}),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import type { VulseDb } from '../../core/db.js'
|
|
3
|
+
import type { Auth } from '../better-auth.js'
|
|
4
|
+
import type { AuthContext, Role } from '../../core/blueprints/types.js'
|
|
5
|
+
import { MediaRepo, type MediaRow } from '../../core/repos/media.js'
|
|
6
|
+
import { defineHandler } from '../handler.js'
|
|
7
|
+
import { putToR2, deleteFromR2 } from '../r2.js'
|
|
8
|
+
import { probeDimensions } from '../image-probe.js'
|
|
9
|
+
import { buildDeliveryUrl, type CfImagesConfig } from '../cf-images.js'
|
|
10
|
+
import { AccessDeniedError, NotFoundError, ValidationError } from '../../core/errors.js'
|
|
11
|
+
import { fail, ok } from '../envelope.js'
|
|
12
|
+
|
|
13
|
+
export interface MediaEnv {
|
|
14
|
+
bucket: R2Bucket
|
|
15
|
+
cfImages: CfImagesConfig
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MediaItem extends MediaRow {
|
|
19
|
+
deliveryUrl: string | null
|
|
20
|
+
previewUrl: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const paramsId = z.object({ id: z.string() })
|
|
24
|
+
|
|
25
|
+
const ALLOWED_MIME = new Set([
|
|
26
|
+
'image/jpeg',
|
|
27
|
+
'image/png',
|
|
28
|
+
'image/gif',
|
|
29
|
+
'image/webp',
|
|
30
|
+
'image/avif',
|
|
31
|
+
'image/svg+xml',
|
|
32
|
+
])
|
|
33
|
+
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024 // 25 MB
|
|
34
|
+
|
|
35
|
+
export function mediaRoutes(db: VulseDb, auth: Auth, mediaEnv: MediaEnv) {
|
|
36
|
+
const repo = new MediaRepo(db)
|
|
37
|
+
|
|
38
|
+
function withUrls(row: MediaRow): MediaItem {
|
|
39
|
+
const deliveryUrl = buildDeliveryUrl(mediaEnv.cfImages, row.id)
|
|
40
|
+
return {
|
|
41
|
+
...row,
|
|
42
|
+
deliveryUrl,
|
|
43
|
+
previewUrl: deliveryUrl ?? `/api/vulse/media/${row.id}/file`,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function requireRole(request: Request, roles: Role[]): Promise<AuthContext> {
|
|
48
|
+
const session = await auth.api.getSession({ headers: request.headers })
|
|
49
|
+
const authCtx: AuthContext = session ? {
|
|
50
|
+
user: {
|
|
51
|
+
id: session.user.id,
|
|
52
|
+
email: session.user.email,
|
|
53
|
+
role: (session.user as { role?: Role }).role ?? 'member',
|
|
54
|
+
},
|
|
55
|
+
} : { user: null }
|
|
56
|
+
if (!authCtx.user) throw new AccessDeniedError('Authentication required')
|
|
57
|
+
if (!roles.includes(authCtx.user.role)) {
|
|
58
|
+
throw new AccessDeniedError(`Requires role: ${roles.join(' or ')}`)
|
|
59
|
+
}
|
|
60
|
+
return authCtx
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
list: defineHandler(auth, { requireRole: ['admin', 'editor'] }, async () => {
|
|
65
|
+
return (await repo.list({})).map(withUrls)
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
upload: async (request: Request): Promise<Response> => {
|
|
69
|
+
try {
|
|
70
|
+
const authCtx = await requireRole(request, ['admin', 'editor'])
|
|
71
|
+
const form = await request.formData()
|
|
72
|
+
const entry = form.get('file')
|
|
73
|
+
if (!entry || typeof entry === 'string') throw new ValidationError('file required')
|
|
74
|
+
const file = entry as File
|
|
75
|
+
if (!ALLOWED_MIME.has(file.type)) {
|
|
76
|
+
throw new ValidationError(`Unsupported file type: ${file.type || 'unknown'}`)
|
|
77
|
+
}
|
|
78
|
+
if (file.size > MAX_UPLOAD_BYTES) {
|
|
79
|
+
throw new ValidationError(`File too large (max ${MAX_UPLOAD_BYTES} bytes)`)
|
|
80
|
+
}
|
|
81
|
+
const buf = await file.arrayBuffer()
|
|
82
|
+
if (buf.byteLength > MAX_UPLOAD_BYTES) {
|
|
83
|
+
throw new ValidationError(`File too large (max ${MAX_UPLOAD_BYTES} bytes)`)
|
|
84
|
+
}
|
|
85
|
+
const dims = probeDimensions(buf, file.type)
|
|
86
|
+
const { key } = await putToR2({ bucket: mediaEnv.bucket }, buf, file.type)
|
|
87
|
+
const row = await repo.create({
|
|
88
|
+
r2Key: key,
|
|
89
|
+
mime: file.type,
|
|
90
|
+
size: file.size,
|
|
91
|
+
...(dims?.width !== undefined ? { width: dims.width } : {}),
|
|
92
|
+
...(dims?.height !== undefined ? { height: dims.height } : {}),
|
|
93
|
+
uploadedBy: authCtx.user!.id,
|
|
94
|
+
})
|
|
95
|
+
return ok(withUrls(row))
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return fail(err)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
updateAlt: defineHandler(auth, {
|
|
102
|
+
params: paramsId,
|
|
103
|
+
body: z.object({ alt: z.string() }),
|
|
104
|
+
requireRole: ['admin', 'editor'],
|
|
105
|
+
}, async ({ params, body }) => {
|
|
106
|
+
await repo.updateAlt(params.id, body.alt)
|
|
107
|
+
return { ok: true }
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
delete: defineHandler(auth, {
|
|
111
|
+
params: paramsId,
|
|
112
|
+
requireRole: ['admin', 'editor'],
|
|
113
|
+
}, async ({ params }) => {
|
|
114
|
+
await repo.softDelete(params.id)
|
|
115
|
+
return { ok: true }
|
|
116
|
+
}),
|
|
117
|
+
|
|
118
|
+
file: async (request: Request, rawParams: Record<string, string>): Promise<Response> => {
|
|
119
|
+
try {
|
|
120
|
+
await requireRole(request, ['admin', 'editor'])
|
|
121
|
+
const id = rawParams.id
|
|
122
|
+
if (!id) throw new ValidationError('id required')
|
|
123
|
+
const row = await repo.findById(id)
|
|
124
|
+
if (!row || row.deletedAt) throw new NotFoundError(`Media ${id} not found`)
|
|
125
|
+
const obj = await mediaEnv.bucket.get(row.r2Key)
|
|
126
|
+
if (!obj) throw new NotFoundError(`Media file ${id} not found`)
|
|
127
|
+
const headers = new Headers()
|
|
128
|
+
if (obj.httpMetadata?.contentType) headers.set('content-type', obj.httpMetadata.contentType)
|
|
129
|
+
headers.set('cache-control', 'private, max-age=3600')
|
|
130
|
+
return new Response(obj.body, { headers })
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return fail(err)
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
purge: async (): Promise<{ purged: number }> => {
|
|
137
|
+
const rows = await repo.listPurgeable(7)
|
|
138
|
+
for (const r of rows) {
|
|
139
|
+
await deleteFromR2({ bucket: mediaEnv.bucket }, r.r2Key)
|
|
140
|
+
await repo.hardDelete(r.id)
|
|
141
|
+
}
|
|
142
|
+
return { purged: rows.length }
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import type { Auth } from '../better-auth.js'
|
|
3
|
+
import type { VulseDb } from '../../core/db.js'
|
|
4
|
+
import type { BlueprintRegistry } from '../../core/blueprints/registry.js'
|
|
5
|
+
import { defineHandler } from '../handler.js'
|
|
6
|
+
import { resolvePreviewPath } from '../../core/blueprints/preview-path.js'
|
|
7
|
+
import { PreviewSessionsRepo } from '../../core/repos/preview-sessions.js'
|
|
8
|
+
import { AccessDeniedError, NotFoundError } from '../../core/errors.js'
|
|
9
|
+
|
|
10
|
+
function buildPreviewUrl(origin: string, pathTemplate: string, slug: string, token: string): string {
|
|
11
|
+
const path = pathTemplate.replace('{slug}', encodeURIComponent(slug))
|
|
12
|
+
const url = new URL(path, origin)
|
|
13
|
+
url.searchParams.set('vulse_live_preview', token)
|
|
14
|
+
return url.toString()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function previewSessionsRoutes(db: VulseDb, auth: Auth, registry: BlueprintRegistry) {
|
|
18
|
+
const repo = new PreviewSessionsRepo(db)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
create: defineHandler(auth, {
|
|
22
|
+
requireRole: ['admin', 'editor'],
|
|
23
|
+
body: z.object({
|
|
24
|
+
collection: z.string(),
|
|
25
|
+
entryId: z.string().nullable().optional(),
|
|
26
|
+
slug: z.string(),
|
|
27
|
+
content: z.record(z.string(), z.unknown()),
|
|
28
|
+
locale: z.string().optional(),
|
|
29
|
+
}),
|
|
30
|
+
}, async ({ auth: ctx, body, url }) => {
|
|
31
|
+
const bp = registry.get(body.collection)
|
|
32
|
+
if (!bp) throw new NotFoundError(`Collection ${body.collection} not found`)
|
|
33
|
+
const row = await repo.create({
|
|
34
|
+
userId: ctx.user!.id,
|
|
35
|
+
collection: body.collection,
|
|
36
|
+
slug: body.slug,
|
|
37
|
+
content: body.content,
|
|
38
|
+
entryId: body.entryId ?? null,
|
|
39
|
+
...(body.locale !== undefined ? { locale: body.locale } : {}),
|
|
40
|
+
})
|
|
41
|
+
const previewPath = resolvePreviewPath(bp)
|
|
42
|
+
return {
|
|
43
|
+
id: row.id,
|
|
44
|
+
previewUrl: buildPreviewUrl(url.origin, previewPath, row.slug, row.id),
|
|
45
|
+
expiresAt: row.expiresAt.toISOString(),
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
|
|
49
|
+
update: defineHandler(auth, {
|
|
50
|
+
requireRole: ['admin', 'editor'],
|
|
51
|
+
params: z.object({ id: z.string() }),
|
|
52
|
+
body: z.object({
|
|
53
|
+
slug: z.string().optional(),
|
|
54
|
+
content: z.record(z.string(), z.unknown()).optional(),
|
|
55
|
+
locale: z.string().optional(),
|
|
56
|
+
}),
|
|
57
|
+
}, async ({ auth: ctx, params, body }) => {
|
|
58
|
+
const patch: { slug?: string; content?: unknown; locale?: string } = {}
|
|
59
|
+
if (body.slug !== undefined) patch.slug = body.slug
|
|
60
|
+
if (body.content !== undefined) patch.content = body.content
|
|
61
|
+
if (body.locale !== undefined) patch.locale = body.locale
|
|
62
|
+
const updated = await repo.update(params.id, ctx.user!.id, patch)
|
|
63
|
+
if (!updated) throw new AccessDeniedError('Session not found or not owned by you')
|
|
64
|
+
return { expiresAt: updated.expiresAt.toISOString() }
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
remove: defineHandler(auth, {
|
|
68
|
+
requireRole: ['admin', 'editor'],
|
|
69
|
+
params: z.object({ id: z.string() }),
|
|
70
|
+
}, async ({ auth: ctx, params }) => {
|
|
71
|
+
const ok = await repo.delete(params.id, ctx.user!.id)
|
|
72
|
+
if (!ok) throw new NotFoundError('Session not found')
|
|
73
|
+
return { ok: true }
|
|
74
|
+
}),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Auth } from '../better-auth.js'
|
|
2
|
+
import { defineHandler } from '../handler.js'
|
|
3
|
+
import { mintPreviewToken } from '../preview.js'
|
|
4
|
+
|
|
5
|
+
function safePreviewTarget(raw: string | null, origin: string): URL {
|
|
6
|
+
const fallback = new URL('/', origin)
|
|
7
|
+
if (!raw) return fallback
|
|
8
|
+
// Reject anything that could escape origin: protocol-relative URLs (//evil.com)
|
|
9
|
+
// and any input that isn't a single leading "/" path.
|
|
10
|
+
if (!raw.startsWith('/') || raw.startsWith('//')) return fallback
|
|
11
|
+
try {
|
|
12
|
+
const candidate = new URL(raw, origin)
|
|
13
|
+
if (candidate.origin !== new URL(origin).origin) return fallback
|
|
14
|
+
return candidate
|
|
15
|
+
} catch {
|
|
16
|
+
return fallback
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function previewRoutes(auth: Auth, secret: string) {
|
|
21
|
+
return {
|
|
22
|
+
start: defineHandler(auth, { requireRole: ['admin', 'editor'] }, async ({ auth: authCtx, url }) => {
|
|
23
|
+
const token = await mintPreviewToken(secret, authCtx.user!.id)
|
|
24
|
+
const secure = url.protocol === 'https:'
|
|
25
|
+
const cookie = `vulse_preview=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600${secure ? '; Secure' : ''}`
|
|
26
|
+
const redirect = safePreviewTarget(url.searchParams.get('to'), url.origin)
|
|
27
|
+
return new Response(null, { status: 302, headers: { Location: redirect.toString(), 'Set-Cookie': cookie } })
|
|
28
|
+
}),
|
|
29
|
+
stop: defineHandler(auth, {}, async ({ url }) => {
|
|
30
|
+
return new Response(null, {
|
|
31
|
+
status: 302,
|
|
32
|
+
headers: { Location: new URL('/', url.origin).toString(), 'Set-Cookie': 'vulse_preview=; Path=/; Max-Age=0' },
|
|
33
|
+
})
|
|
34
|
+
}),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import type { VulseDb } from '../../core/db.js'
|
|
3
|
+
import type { Auth } from '../better-auth.js'
|
|
4
|
+
import { RevisionsRepo } from '../../core/repos/revisions.js'
|
|
5
|
+
import { resolveLocale } from '../../core/locales.js'
|
|
6
|
+
import { defineHandler } from '../handler.js'
|
|
7
|
+
|
|
8
|
+
export function revisionsRoutes(db: VulseDb, auth: Auth) {
|
|
9
|
+
const repo = new RevisionsRepo(db)
|
|
10
|
+
return {
|
|
11
|
+
list: defineHandler(auth, {
|
|
12
|
+
params: z.object({ collection: z.string(), id: z.string() }),
|
|
13
|
+
requireRole: ['admin', 'editor'],
|
|
14
|
+
}, async ({ params, url }) => {
|
|
15
|
+
const locale = await resolveLocale(db, url.searchParams.get('locale'))
|
|
16
|
+
return await repo.listByEntry(params.id, locale)
|
|
17
|
+
}),
|
|
18
|
+
|
|
19
|
+
restore: defineHandler(auth, {
|
|
20
|
+
params: z.object({ collection: z.string(), id: z.string(), version: z.string() }),
|
|
21
|
+
requireRole: ['admin', 'editor'],
|
|
22
|
+
}, async ({ params, url, auth: authCtx }) => {
|
|
23
|
+
if (!authCtx.user) throw new Error('unreachable')
|
|
24
|
+
const locale = await resolveLocale(db, url.searchParams.get('locale'))
|
|
25
|
+
await repo.restore(params.id, Number(params.version), { userId: authCtx.user.id, locale })
|
|
26
|
+
return { restored: Number(params.version) }
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import type { VulseDb } from '../../core/db.js'
|
|
3
|
+
import type { Auth } from '../better-auth.js'
|
|
4
|
+
import { defineHandler } from '../handler.js'
|
|
5
|
+
import { searchSdk } from '../sdk/search.js'
|
|
6
|
+
|
|
7
|
+
export function searchRoutes(db: VulseDb, auth: Auth) {
|
|
8
|
+
const sdk = searchSdk(db)
|
|
9
|
+
return {
|
|
10
|
+
query: defineHandler(auth, {
|
|
11
|
+
params: z.object({}),
|
|
12
|
+
body: z.object({
|
|
13
|
+
q: z.string(),
|
|
14
|
+
collections: z.array(z.string()).optional(),
|
|
15
|
+
limit: z.number().optional(),
|
|
16
|
+
includeDrafts: z.boolean().optional(),
|
|
17
|
+
locale: z.string().optional(),
|
|
18
|
+
}),
|
|
19
|
+
}, async ({ body, auth: authCtx }) => {
|
|
20
|
+
const role = authCtx.user?.role
|
|
21
|
+
const mayReadDrafts = role === 'admin' || role === 'editor'
|
|
22
|
+
const includeDrafts = body.includeDrafts === true && mayReadDrafts
|
|
23
|
+
return sdk.query(body.q, {
|
|
24
|
+
...(body.collections !== undefined ? { collections: body.collections } : {}),
|
|
25
|
+
...(body.limit !== undefined ? { limit: body.limit } : {}),
|
|
26
|
+
...(body.locale !== undefined ? { locale: body.locale } : {}),
|
|
27
|
+
includeDrafts,
|
|
28
|
+
})
|
|
29
|
+
}),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import type { Auth } from '../better-auth.js'
|
|
3
|
+
import type { VulseDb } from '../../core/db.js'
|
|
4
|
+
import { SetDefinitionSchema } from '../../core/sets/definition.js'
|
|
5
|
+
import { createSet, deleteSet, getSet, listSets, updateSet } from '../../core/sets/service.js'
|
|
6
|
+
import { NotFoundError } from '../../core/errors.js'
|
|
7
|
+
import { defineHandler } from '../handler.js'
|
|
8
|
+
|
|
9
|
+
const paramsHandle = z.object({ handle: z.string() })
|
|
10
|
+
|
|
11
|
+
export function setsRoutes(db: VulseDb, auth: Auth) {
|
|
12
|
+
return {
|
|
13
|
+
list: defineHandler(auth, {}, async () => listSets(db)),
|
|
14
|
+
|
|
15
|
+
get: defineHandler(auth, { params: paramsHandle }, async ({ params }) => {
|
|
16
|
+
const row = await getSet(db, params.handle)
|
|
17
|
+
if (!row) throw new NotFoundError('set not found')
|
|
18
|
+
return row
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
create: defineHandler(auth, {
|
|
22
|
+
body: SetDefinitionSchema,
|
|
23
|
+
requireRole: ['admin'],
|
|
24
|
+
}, async ({ body }) => createSet(db, body)),
|
|
25
|
+
|
|
26
|
+
update: defineHandler(auth, {
|
|
27
|
+
params: paramsHandle,
|
|
28
|
+
body: SetDefinitionSchema,
|
|
29
|
+
requireRole: ['admin'],
|
|
30
|
+
}, async ({ params, body }) => updateSet(db, params.handle, body)),
|
|
31
|
+
|
|
32
|
+
delete: defineHandler(auth, {
|
|
33
|
+
params: paramsHandle,
|
|
34
|
+
requireRole: ['admin'],
|
|
35
|
+
}, async ({ params }) => {
|
|
36
|
+
await deleteSet(db, params.handle)
|
|
37
|
+
return null
|
|
38
|
+
}),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import type { VulseDb } from '../../core/db.js'
|
|
3
|
+
import type { Auth } from '../better-auth.js'
|
|
4
|
+
import { defineHandler } from '../handler.js'
|
|
5
|
+
import { SettingsRepo } from '../../core/repos/settings.js'
|
|
6
|
+
import { invalidateRuntime } from '../runtime.js'
|
|
7
|
+
|
|
8
|
+
const AUTH_SETTING_KEYS = new Set(['allowMemberSignUp', 'allowedSignUpDomains'])
|
|
9
|
+
|
|
10
|
+
export function settingsRoutes(db: VulseDb, auth: Auth) {
|
|
11
|
+
const repo = new SettingsRepo(db)
|
|
12
|
+
return {
|
|
13
|
+
list: defineHandler(auth, { requireRole: ['admin'] }, async () => await repo.all()),
|
|
14
|
+
set: defineHandler(auth, {
|
|
15
|
+
params: z.object({ key: z.string() }),
|
|
16
|
+
body: z.object({ value: z.unknown() }),
|
|
17
|
+
requireRole: ['admin'],
|
|
18
|
+
}, async ({ params, body }) => {
|
|
19
|
+
await repo.set(params.key, body.value)
|
|
20
|
+
if (AUTH_SETTING_KEYS.has(params.key)) invalidateRuntime()
|
|
21
|
+
return { key: params.key }
|
|
22
|
+
}),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
import { and, eq, like, or } from 'drizzle-orm'
|
|
3
|
+
import { hashPassword } from 'better-auth/crypto'
|
|
4
|
+
import { nanoid } from 'nanoid'
|
|
5
|
+
import type { VulseDb } from '../../core/db.js'
|
|
6
|
+
import type { Auth } from '../better-auth.js'
|
|
7
|
+
import { account as accountTable, user as userTable } from '../../core/schema.js'
|
|
8
|
+
import { defineHandler } from '../handler.js'
|
|
9
|
+
import { NotFoundError } from '../../core/errors.js'
|
|
10
|
+
|
|
11
|
+
const userFields = {
|
|
12
|
+
id: userTable.id,
|
|
13
|
+
email: userTable.email,
|
|
14
|
+
name: userTable.name,
|
|
15
|
+
role: userTable.role,
|
|
16
|
+
displayName: userTable.displayName,
|
|
17
|
+
emailVerified: userTable.emailVerified,
|
|
18
|
+
createdAt: userTable.createdAt,
|
|
19
|
+
updatedAt: userTable.updatedAt,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function usersRoutes(db: VulseDb, auth: Auth) {
|
|
23
|
+
return {
|
|
24
|
+
list: defineHandler(auth, { requireRole: ['admin'] }, async ({ url }) => {
|
|
25
|
+
const q = url.searchParams.get('q')?.trim()
|
|
26
|
+
if (q) {
|
|
27
|
+
const pattern = `%${q}%`
|
|
28
|
+
return await db.select(userFields).from(userTable).where(or(
|
|
29
|
+
like(userTable.email, pattern),
|
|
30
|
+
like(userTable.name, pattern),
|
|
31
|
+
like(userTable.displayName, pattern),
|
|
32
|
+
eq(userTable.id, q),
|
|
33
|
+
))
|
|
34
|
+
}
|
|
35
|
+
return await db.select(userFields).from(userTable)
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
get: defineHandler(auth, {
|
|
39
|
+
params: z.object({ id: z.string() }),
|
|
40
|
+
requireRole: ['admin'],
|
|
41
|
+
}, async ({ params }) => {
|
|
42
|
+
const rows = await db.select(userFields).from(userTable).where(eq(userTable.id, params.id))
|
|
43
|
+
if (!rows.length) throw new NotFoundError(`User ${params.id} not found`)
|
|
44
|
+
return rows[0]
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
update: defineHandler(auth, {
|
|
48
|
+
params: z.object({ id: z.string() }),
|
|
49
|
+
body: z.object({
|
|
50
|
+
name: z.string().min(1).optional(),
|
|
51
|
+
displayName: z.string().nullable().optional(),
|
|
52
|
+
role: z.enum(['admin', 'editor', 'member']).optional(),
|
|
53
|
+
}),
|
|
54
|
+
requireRole: ['admin'],
|
|
55
|
+
}, async ({ params, body }) => {
|
|
56
|
+
const existing = await db.select({ id: userTable.id }).from(userTable).where(eq(userTable.id, params.id))
|
|
57
|
+
if (!existing.length) throw new NotFoundError(`User ${params.id} not found`)
|
|
58
|
+
|
|
59
|
+
const patch: Partial<typeof userTable.$inferInsert> = { updatedAt: new Date() }
|
|
60
|
+
if (body.name !== undefined) patch.name = body.name
|
|
61
|
+
if (body.displayName !== undefined) patch.displayName = body.displayName
|
|
62
|
+
if (body.role !== undefined) patch.role = body.role
|
|
63
|
+
|
|
64
|
+
await db.update(userTable).set(patch).where(eq(userTable.id, params.id))
|
|
65
|
+
const rows = await db.select(userFields).from(userTable).where(eq(userTable.id, params.id))
|
|
66
|
+
return rows[0]
|
|
67
|
+
}),
|
|
68
|
+
|
|
69
|
+
setRole: defineHandler(auth, {
|
|
70
|
+
params: z.object({ id: z.string() }),
|
|
71
|
+
body: z.object({ role: z.enum(['admin', 'editor', 'member']) }),
|
|
72
|
+
requireRole: ['admin'],
|
|
73
|
+
}, async ({ params, body }) => {
|
|
74
|
+
const existing = await db.select({ id: userTable.id }).from(userTable).where(eq(userTable.id, params.id))
|
|
75
|
+
if (!existing.length) throw new NotFoundError(`User ${params.id} not found`)
|
|
76
|
+
await db.update(userTable).set({ role: body.role, updatedAt: new Date() }).where(eq(userTable.id, params.id))
|
|
77
|
+
return { id: params.id, role: body.role }
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
resetPassword: defineHandler(auth, {
|
|
81
|
+
params: z.object({ id: z.string() }),
|
|
82
|
+
body: z.discriminatedUnion('action', [
|
|
83
|
+
z.object({ action: z.literal('email'), redirectTo: z.string().optional() }),
|
|
84
|
+
z.object({ action: z.literal('set'), password: z.string().min(8) }),
|
|
85
|
+
]),
|
|
86
|
+
requireRole: ['admin'],
|
|
87
|
+
}, async ({ params, body, request }) => {
|
|
88
|
+
const rows = await db.select({ id: userTable.id, email: userTable.email }).from(userTable).where(eq(userTable.id, params.id))
|
|
89
|
+
if (!rows.length) throw new NotFoundError(`User ${params.id} not found`)
|
|
90
|
+
const target = rows[0]!
|
|
91
|
+
|
|
92
|
+
if (body.action === 'email') {
|
|
93
|
+
await auth.api.requestPasswordReset({
|
|
94
|
+
body: {
|
|
95
|
+
email: target.email,
|
|
96
|
+
redirectTo: body.redirectTo ?? '/reset-password',
|
|
97
|
+
},
|
|
98
|
+
headers: request.headers,
|
|
99
|
+
})
|
|
100
|
+
return { action: 'email' as const, sent: true }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const hashedPassword = await hashPassword(body.password)
|
|
104
|
+
const accounts = await db.select({ id: accountTable.id }).from(accountTable).where(and(
|
|
105
|
+
eq(accountTable.userId, params.id),
|
|
106
|
+
eq(accountTable.providerId, 'credential'),
|
|
107
|
+
))
|
|
108
|
+
|
|
109
|
+
const now = new Date()
|
|
110
|
+
if (accounts.length) {
|
|
111
|
+
await db.update(accountTable).set({ password: hashedPassword, updatedAt: now }).where(eq(accountTable.id, accounts[0]!.id))
|
|
112
|
+
} else {
|
|
113
|
+
await db.insert(accountTable).values({
|
|
114
|
+
id: nanoid(),
|
|
115
|
+
userId: params.id,
|
|
116
|
+
accountId: params.id,
|
|
117
|
+
providerId: 'credential',
|
|
118
|
+
password: hashedPassword,
|
|
119
|
+
createdAt: now,
|
|
120
|
+
updatedAt: now,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { action: 'set' as const, updated: true }
|
|
125
|
+
}),
|
|
126
|
+
}
|
|
127
|
+
}
|