@jant/core 0.3.35 → 0.3.37
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/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multipart Upload API Routes
|
|
3
|
+
*
|
|
4
|
+
* Handles chunked file uploads for files that exceed the Cloudflare Workers
|
|
5
|
+
* 100MB request body limit. Uses R2's native multipart upload API.
|
|
6
|
+
*
|
|
7
|
+
* Protocol:
|
|
8
|
+
* 1. POST / — Initiate: validate metadata, start R2 multipart upload
|
|
9
|
+
* 2. PUT /:id/part — Upload a single chunk (raw body, not FormData)
|
|
10
|
+
* 3. POST /:id/complete — Finalize: combine parts in R2, create DB record
|
|
11
|
+
* 4. POST /:id/abort — Cancel: discard uploaded parts
|
|
12
|
+
* 5. PUT /:id/poster — Upload poster frame (video thumbnails, small FormData)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Hono } from "hono";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import type { Bindings } from "../../types.js";
|
|
18
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
19
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
20
|
+
import { getMediaUrl, getPublicUrlForProvider } from "../../lib/image.js";
|
|
21
|
+
import {
|
|
22
|
+
validateUploadFileMetadata,
|
|
23
|
+
generateStorageKey,
|
|
24
|
+
} from "../../lib/upload.js";
|
|
25
|
+
import { supportsMultipart } from "../../lib/storage.js";
|
|
26
|
+
import { ValidationError } from "../../lib/errors.js";
|
|
27
|
+
import { parseValidated } from "../../lib/schemas.js";
|
|
28
|
+
|
|
29
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
30
|
+
|
|
31
|
+
// ── Schemas ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const InitiateSchema = z.object({
|
|
34
|
+
filename: z.string().min(1),
|
|
35
|
+
contentType: z.string().min(1),
|
|
36
|
+
size: z.number().int().positive(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const UploadPartSchema = z.object({
|
|
40
|
+
storageKey: z.string().min(1),
|
|
41
|
+
uploadId: z.string().min(1),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const CompleteSchema = z.object({
|
|
45
|
+
storageKey: z.string().min(1),
|
|
46
|
+
uploadId: z.string().min(1),
|
|
47
|
+
parts: z.array(
|
|
48
|
+
z.object({
|
|
49
|
+
partNumber: z.number().int().positive(),
|
|
50
|
+
etag: z.string().min(1),
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
filename: z.string().min(1),
|
|
54
|
+
originalName: z.string().min(1),
|
|
55
|
+
contentType: z.string().min(1),
|
|
56
|
+
size: z.number().int().positive(),
|
|
57
|
+
width: z.number().int().positive().optional(),
|
|
58
|
+
height: z.number().int().positive().optional(),
|
|
59
|
+
blurhash: z.string().max(200).optional(),
|
|
60
|
+
waveform: z.string().max(2000).optional(),
|
|
61
|
+
posterKey: z.string().optional(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const AbortSchema = z.object({
|
|
65
|
+
storageKey: z.string().min(1),
|
|
66
|
+
uploadId: z.string().min(1),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── Routes ───────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export const multipartUploadApiRoutes = new Hono<Env>();
|
|
72
|
+
|
|
73
|
+
// Require auth for all multipart routes
|
|
74
|
+
multipartUploadApiRoutes.use("*", requireAuthApi());
|
|
75
|
+
|
|
76
|
+
// POST / — Initiate a multipart upload
|
|
77
|
+
multipartUploadApiRoutes.post("/", async (c) => {
|
|
78
|
+
const storage = c.var.storage;
|
|
79
|
+
if (!storage || !supportsMultipart(storage)) {
|
|
80
|
+
return c.json({ error: "Storage doesn't support multipart uploads." }, 500);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const body = await c.req.json();
|
|
84
|
+
const data = parseValidated(InitiateSchema, body);
|
|
85
|
+
|
|
86
|
+
// Validate file type and size
|
|
87
|
+
const error = validateUploadFileMetadata(data.contentType, data.size, {
|
|
88
|
+
maxFileSizeMB: c.var.appConfig.uploadMaxFileSize,
|
|
89
|
+
});
|
|
90
|
+
if (error) {
|
|
91
|
+
throw new ValidationError(error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { id, filename, storageKey } = generateStorageKey(data.filename);
|
|
95
|
+
|
|
96
|
+
const upload = await storage.createMultipartUpload(storageKey, {
|
|
97
|
+
contentType: data.contentType,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return c.json({
|
|
101
|
+
id,
|
|
102
|
+
uploadId: upload.uploadId,
|
|
103
|
+
storageKey,
|
|
104
|
+
filename,
|
|
105
|
+
originalName: data.filename,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// PUT /:id/part?partNumber=N&storageKey=...&uploadId=... — Upload a single part
|
|
110
|
+
multipartUploadApiRoutes.put("/:id/part", async (c) => {
|
|
111
|
+
const storage = c.var.storage;
|
|
112
|
+
if (!storage || !supportsMultipart(storage)) {
|
|
113
|
+
return c.json({ error: "Storage doesn't support multipart uploads." }, 500);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const storageKey = c.req.query("storageKey");
|
|
117
|
+
const uploadId = c.req.query("uploadId");
|
|
118
|
+
if (!storageKey || !uploadId) {
|
|
119
|
+
throw new ValidationError(
|
|
120
|
+
"storageKey and uploadId query parameters are required",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
parseValidated(UploadPartSchema, { storageKey, uploadId });
|
|
124
|
+
|
|
125
|
+
const partNumberRaw = c.req.query("partNumber");
|
|
126
|
+
if (!partNumberRaw) {
|
|
127
|
+
throw new ValidationError("partNumber query parameter is required");
|
|
128
|
+
}
|
|
129
|
+
const partNumber = parseInt(partNumberRaw, 10);
|
|
130
|
+
if (isNaN(partNumber) || partNumber < 1) {
|
|
131
|
+
throw new ValidationError("partNumber must be a positive integer");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const body = await c.req.arrayBuffer();
|
|
135
|
+
const part = await storage.uploadPart(storageKey, uploadId, partNumber, body);
|
|
136
|
+
|
|
137
|
+
return c.json({ partNumber: part.partNumber, etag: part.etag });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// POST /:id/complete — Finalize the upload
|
|
141
|
+
multipartUploadApiRoutes.post("/:id/complete", async (c) => {
|
|
142
|
+
const storage = c.var.storage;
|
|
143
|
+
if (!storage || !supportsMultipart(storage)) {
|
|
144
|
+
return c.json({ error: "Storage doesn't support multipart uploads." }, 500);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const id = c.req.param("id");
|
|
148
|
+
const body = await c.req.json();
|
|
149
|
+
const data = parseValidated(CompleteSchema, body);
|
|
150
|
+
|
|
151
|
+
// Validate file type and size
|
|
152
|
+
const validationError = validateUploadFileMetadata(
|
|
153
|
+
data.contentType,
|
|
154
|
+
data.size,
|
|
155
|
+
{ maxFileSizeMB: c.var.appConfig.uploadMaxFileSize },
|
|
156
|
+
);
|
|
157
|
+
if (validationError) {
|
|
158
|
+
throw new ValidationError(validationError);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Complete the R2 multipart upload
|
|
162
|
+
await storage.completeMultipartUpload(
|
|
163
|
+
data.storageKey,
|
|
164
|
+
data.uploadId,
|
|
165
|
+
data.parts,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Create the DB record
|
|
169
|
+
const media = await c.var.services.media.create({
|
|
170
|
+
id,
|
|
171
|
+
filename: data.filename,
|
|
172
|
+
originalName: data.originalName,
|
|
173
|
+
mimeType: data.contentType,
|
|
174
|
+
size: data.size,
|
|
175
|
+
storageKey: data.storageKey,
|
|
176
|
+
provider: c.var.appConfig.storageDriver,
|
|
177
|
+
width: data.width && data.width > 0 ? data.width : undefined,
|
|
178
|
+
height: data.height && data.height > 0 ? data.height : undefined,
|
|
179
|
+
blurhash: data.blurhash,
|
|
180
|
+
waveform: data.waveform,
|
|
181
|
+
posterKey: data.posterKey,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const mediaPublicUrl = getPublicUrlForProvider(
|
|
185
|
+
c.var.appConfig.storageDriver,
|
|
186
|
+
c.var.appConfig.r2PublicUrl,
|
|
187
|
+
c.var.appConfig.s3PublicUrl,
|
|
188
|
+
);
|
|
189
|
+
const publicUrl = getMediaUrl(data.storageKey, mediaPublicUrl);
|
|
190
|
+
|
|
191
|
+
return c.json({
|
|
192
|
+
id: media.id,
|
|
193
|
+
filename: media.filename,
|
|
194
|
+
url: publicUrl,
|
|
195
|
+
mimeType: media.mimeType,
|
|
196
|
+
size: media.size,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// POST /:id/abort — Cancel the upload
|
|
201
|
+
multipartUploadApiRoutes.post("/:id/abort", async (c) => {
|
|
202
|
+
const storage = c.var.storage;
|
|
203
|
+
if (!storage || !supportsMultipart(storage)) {
|
|
204
|
+
return c.json({ error: "Storage doesn't support multipart uploads." }, 500);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const body = await c.req.json();
|
|
208
|
+
const data = parseValidated(AbortSchema, body);
|
|
209
|
+
|
|
210
|
+
await storage.abortMultipartUpload(data.storageKey, data.uploadId);
|
|
211
|
+
|
|
212
|
+
return c.json({ success: true });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// PUT /:id/poster — Upload poster frame (video thumbnails)
|
|
216
|
+
multipartUploadApiRoutes.put("/:id/poster", async (c) => {
|
|
217
|
+
const storage = c.var.storage;
|
|
218
|
+
if (!storage) {
|
|
219
|
+
return c.json({ error: "Storage not configured." }, 500);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const id = c.req.param("id");
|
|
223
|
+
const formData = await c.req.formData();
|
|
224
|
+
const posterFile = formData.get("poster") as File | null;
|
|
225
|
+
if (!posterFile) {
|
|
226
|
+
throw new ValidationError("No poster file provided");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!posterFile.type.startsWith("image/")) {
|
|
230
|
+
throw new ValidationError(
|
|
231
|
+
`Invalid file type "${posterFile.type}". Only image files are accepted for poster frames.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const date = new Date();
|
|
236
|
+
const year = date.getUTCFullYear();
|
|
237
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
238
|
+
const posterKey = `media/${year}/${month}/${id}-poster.webp`;
|
|
239
|
+
|
|
240
|
+
await storage.put(posterKey, posterFile.stream(), {
|
|
241
|
+
contentType: "image/webp",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return c.json({ posterKey });
|
|
245
|
+
});
|
package/src/routes/api/upload.ts
CHANGED
|
@@ -60,7 +60,7 @@ function renderMediaCard(
|
|
|
60
60
|
<button
|
|
61
61
|
type="button"
|
|
62
62
|
class="block w-full aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary cursor-pointer"
|
|
63
|
-
|
|
63
|
+
data-on:click="document.getElementById('lightbox-img').src = '${fullUrl}'; document.getElementById('lightbox').showModal()"
|
|
64
64
|
>
|
|
65
65
|
<img
|
|
66
66
|
src="${thumbnailUrl}"
|
|
@@ -69,13 +69,9 @@ function renderMediaCard(
|
|
|
69
69
|
loading="lazy"
|
|
70
70
|
/>
|
|
71
71
|
</button>
|
|
72
|
-
<
|
|
73
|
-
href="/dash/media/${media.id}"
|
|
74
|
-
class="block mt-2 text-xs truncate hover:underline"
|
|
75
|
-
title="${media.originalName}"
|
|
76
|
-
>
|
|
72
|
+
<span class="block mt-2 text-xs truncate" title="${media.originalName}">
|
|
77
73
|
${media.originalName}
|
|
78
|
-
</
|
|
74
|
+
</span>
|
|
79
75
|
<div class="text-xs text-muted-foreground">${sizeStr}</div>
|
|
80
76
|
</div>
|
|
81
77
|
`.toString();
|
|
@@ -83,23 +79,18 @@ function renderMediaCard(
|
|
|
83
79
|
|
|
84
80
|
return html`
|
|
85
81
|
<div class="group relative" data-media-id="${media.id}">
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
|
|
82
|
+
<div
|
|
83
|
+
class="block aspect-square bg-muted rounded-lg overflow-hidden border"
|
|
89
84
|
>
|
|
90
85
|
<div
|
|
91
86
|
class="w-full h-full flex items-center justify-center text-muted-foreground"
|
|
92
87
|
>
|
|
93
88
|
<span class="text-xs">${media.mimeType}</span>
|
|
94
89
|
</div>
|
|
95
|
-
</
|
|
96
|
-
<
|
|
97
|
-
href="/dash/media/${media.id}"
|
|
98
|
-
class="block mt-2 text-xs truncate hover:underline"
|
|
99
|
-
title="${media.originalName}"
|
|
100
|
-
>
|
|
90
|
+
</div>
|
|
91
|
+
<span class="block mt-2 text-xs truncate" title="${media.originalName}">
|
|
101
92
|
${media.originalName}
|
|
102
|
-
</
|
|
93
|
+
</span>
|
|
103
94
|
<div class="text-xs text-muted-foreground">${sizeStr}</div>
|
|
104
95
|
</div>
|
|
105
96
|
`.toString();
|
|
@@ -138,7 +129,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
138
129
|
if (!storage) {
|
|
139
130
|
const errorText = i18n._(
|
|
140
131
|
msg({
|
|
141
|
-
message: "
|
|
132
|
+
message: "File storage isn't set up. Check your server config.",
|
|
142
133
|
comment: "@context: Error when file storage is not set up",
|
|
143
134
|
}),
|
|
144
135
|
);
|
|
@@ -154,7 +145,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
154
145
|
if (!file) {
|
|
155
146
|
const errorText = i18n._(
|
|
156
147
|
msg({
|
|
157
|
-
message: "No file
|
|
148
|
+
message: "No file selected. Choose a file to upload.",
|
|
158
149
|
comment: "@context: Error when no file was selected for upload",
|
|
159
150
|
}),
|
|
160
151
|
);
|
|
@@ -165,7 +156,9 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
165
156
|
}
|
|
166
157
|
|
|
167
158
|
// Validate file type and size
|
|
168
|
-
const uploadError = validateUploadFile(file
|
|
159
|
+
const uploadError = validateUploadFile(file, {
|
|
160
|
+
maxFileSizeMB: c.var.appConfig.uploadMaxFileSize,
|
|
161
|
+
});
|
|
169
162
|
if (uploadError) {
|
|
170
163
|
if (wantsSSE(c)) {
|
|
171
164
|
return sseUploadError(c, uploadError);
|
|
@@ -177,11 +170,77 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
177
170
|
const { id, filename, storageKey } = generateStorageKey(file.name);
|
|
178
171
|
|
|
179
172
|
try {
|
|
180
|
-
//
|
|
181
|
-
|
|
173
|
+
// Read optional summary (provided for text attachments)
|
|
174
|
+
let summary = (formData.get("summary") as string) || undefined;
|
|
175
|
+
let chars: number | undefined;
|
|
176
|
+
// Buffer for text files — file.stream() may not work after file.text()
|
|
177
|
+
let textBuffer: Uint8Array | undefined;
|
|
178
|
+
|
|
179
|
+
// Extract summary and char count BEFORE consuming the stream for storage,
|
|
180
|
+
// because file.text() may not work after file.stream() is consumed.
|
|
181
|
+
if (
|
|
182
|
+
file.type === "text/plain" ||
|
|
183
|
+
file.type === "text/markdown" ||
|
|
184
|
+
file.type === "text/csv"
|
|
185
|
+
) {
|
|
186
|
+
try {
|
|
187
|
+
const textContent = await file.text();
|
|
188
|
+
textBuffer = new TextEncoder().encode(textContent);
|
|
189
|
+
chars = textContent.length;
|
|
190
|
+
if (!summary) {
|
|
191
|
+
summary = textContent.slice(0, 100).trim() || undefined;
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// Ignore — summary and chars are optional
|
|
195
|
+
}
|
|
196
|
+
} else if (file.type === "text/x-tiptap+json") {
|
|
197
|
+
try {
|
|
198
|
+
const raw = await file.text();
|
|
199
|
+
textBuffer = new TextEncoder().encode(raw);
|
|
200
|
+
const envelope = JSON.parse(raw) as {
|
|
201
|
+
json?: { content?: unknown[] };
|
|
202
|
+
html?: string;
|
|
203
|
+
};
|
|
204
|
+
// Walk the TipTap JSON tree to extract plain text
|
|
205
|
+
if (envelope.json) {
|
|
206
|
+
let text = "";
|
|
207
|
+
const walk = (node: Record<string, unknown>) => {
|
|
208
|
+
if (typeof node.text === "string") text += node.text;
|
|
209
|
+
if (Array.isArray(node.content))
|
|
210
|
+
(node.content as Record<string, unknown>[]).forEach(walk);
|
|
211
|
+
};
|
|
212
|
+
walk(envelope.json as Record<string, unknown>);
|
|
213
|
+
chars = text.length;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Ignore — chars is optional
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Upload to storage — use buffered bytes for text files (stream may be consumed)
|
|
221
|
+
await storage.put(storageKey, textBuffer ?? file.stream(), {
|
|
182
222
|
contentType: file.type,
|
|
183
223
|
});
|
|
184
224
|
|
|
225
|
+
// Read optional client-side metadata
|
|
226
|
+
const widthRaw = parseInt(formData.get("width") as string) || undefined;
|
|
227
|
+
const heightRaw = parseInt(formData.get("height") as string) || undefined;
|
|
228
|
+
const blurhashRaw = (formData.get("blurhash") as string) || undefined;
|
|
229
|
+
const waveformRaw = (formData.get("waveform") as string) || undefined;
|
|
230
|
+
|
|
231
|
+
// Upload poster frame for videos (if provided by client)
|
|
232
|
+
let posterKey: string | undefined;
|
|
233
|
+
const posterFile = formData.get("poster") as File | null;
|
|
234
|
+
if (posterFile && file.type.startsWith("video/")) {
|
|
235
|
+
const date = new Date();
|
|
236
|
+
const year = date.getUTCFullYear();
|
|
237
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
238
|
+
posterKey = `media/${year}/${month}/${id}-poster.webp`;
|
|
239
|
+
await storage.put(posterKey, posterFile.stream(), {
|
|
240
|
+
contentType: "image/webp",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
185
244
|
// Save to database
|
|
186
245
|
const media = await c.var.services.media.create({
|
|
187
246
|
id,
|
|
@@ -191,6 +250,15 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
191
250
|
size: file.size,
|
|
192
251
|
storageKey,
|
|
193
252
|
provider: c.var.appConfig.storageDriver,
|
|
253
|
+
width: widthRaw && widthRaw > 0 ? widthRaw : undefined,
|
|
254
|
+
height: heightRaw && heightRaw > 0 ? heightRaw : undefined,
|
|
255
|
+
blurhash:
|
|
256
|
+
blurhashRaw && blurhashRaw.length < 200 ? blurhashRaw : undefined,
|
|
257
|
+
waveform:
|
|
258
|
+
waveformRaw && waveformRaw.length < 2000 ? waveformRaw : undefined,
|
|
259
|
+
posterKey,
|
|
260
|
+
summary,
|
|
261
|
+
chars,
|
|
194
262
|
});
|
|
195
263
|
|
|
196
264
|
// SSE response for Datastar
|
|
@@ -215,7 +283,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
215
283
|
await stream.toast(
|
|
216
284
|
i18n._(
|
|
217
285
|
msg({
|
|
218
|
-
message: "
|
|
286
|
+
message: "File uploaded.",
|
|
219
287
|
comment: "@context: Toast after successful file upload",
|
|
220
288
|
}),
|
|
221
289
|
),
|
|
@@ -243,7 +311,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
243
311
|
|
|
244
312
|
const errorText = i18n._(
|
|
245
313
|
msg({
|
|
246
|
-
message: "Upload
|
|
314
|
+
message: "Upload didn't go through. Try again in a moment.",
|
|
247
315
|
comment: "@context: Error when file upload fails",
|
|
248
316
|
}),
|
|
249
317
|
);
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
3
|
-
import { createPageService } from "../../../services/page.js";
|
|
4
3
|
import { createSettingsService } from "../../../services/settings.js";
|
|
5
4
|
import { createNavItemService } from "../../../services/navigation.js";
|
|
6
|
-
import { createPathRegistryService } from "../../../services/path-registry.js";
|
|
7
5
|
import type { Database } from "../../../db/index.js";
|
|
8
|
-
import type { PageService } from "../../../services/page.js";
|
|
9
6
|
import type { SettingsService } from "../../../services/settings.js";
|
|
10
7
|
import type { NavItemService } from "../../../services/navigation.js";
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
|
-
* Reproduces the seed logic from POST /setup to verify the default
|
|
14
|
-
*
|
|
10
|
+
* Reproduces the seed logic from POST /setup to verify the default
|
|
11
|
+
* navigation items are created correctly.
|
|
15
12
|
*/
|
|
16
13
|
async function runSetupSeed(services: {
|
|
17
|
-
pages: PageService;
|
|
18
14
|
settings: SettingsService;
|
|
19
15
|
navItems: NavItemService;
|
|
20
16
|
}) {
|
|
@@ -30,31 +26,20 @@ async function runSetupSeed(services: {
|
|
|
30
26
|
label: "Archive",
|
|
31
27
|
url: "/archive",
|
|
32
28
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
body: [
|
|
38
|
-
"Welcome to my corner of the internet.",
|
|
39
|
-
"",
|
|
40
|
-
"This is a place where I share my thoughts, ideas, and things I find interesting. Feel free to look around and get to know what this site is all about.",
|
|
41
|
-
"",
|
|
42
|
-
"If you'd like to get in touch, don't hesitate to reach out.",
|
|
43
|
-
].join("\n"),
|
|
44
|
-
status: "published",
|
|
29
|
+
await services.navItems.create({
|
|
30
|
+
type: "system",
|
|
31
|
+
label: "RSS",
|
|
32
|
+
url: "/feed",
|
|
45
33
|
});
|
|
46
|
-
|
|
47
34
|
await services.navItems.create({
|
|
48
|
-
type: "
|
|
49
|
-
label: "
|
|
50
|
-
url: "/
|
|
51
|
-
pageId: aboutPage.id,
|
|
35
|
+
type: "system",
|
|
36
|
+
label: "Settings",
|
|
37
|
+
url: "/settings",
|
|
52
38
|
});
|
|
53
39
|
}
|
|
54
40
|
|
|
55
41
|
describe("Setup seed logic", () => {
|
|
56
42
|
let services: {
|
|
57
|
-
pages: PageService;
|
|
58
43
|
settings: SettingsService;
|
|
59
44
|
navItems: NavItemService;
|
|
60
45
|
};
|
|
@@ -63,57 +48,32 @@ describe("Setup seed logic", () => {
|
|
|
63
48
|
const testDb = createTestDatabase();
|
|
64
49
|
const db = testDb.db as unknown as Database;
|
|
65
50
|
services = {
|
|
66
|
-
pages: createPageService(db, createPathRegistryService(db)),
|
|
67
51
|
settings: createSettingsService(db),
|
|
68
52
|
navItems: createNavItemService(db),
|
|
69
53
|
};
|
|
70
54
|
});
|
|
71
55
|
|
|
72
|
-
it("creates
|
|
73
|
-
await runSetupSeed(services);
|
|
74
|
-
|
|
75
|
-
const aboutPage = await services.pages.getBySlug("about");
|
|
76
|
-
expect(aboutPage).not.toBeNull();
|
|
77
|
-
expect(aboutPage?.title).toBe("About");
|
|
78
|
-
expect(aboutPage?.status).toBe("published");
|
|
79
|
-
expect(aboutPage?.body).toContain("Welcome to my corner of the internet");
|
|
80
|
-
expect(aboutPage?.bodyHtml).toBeTruthy();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("adds About page to navigation as a page-type nav item", async () => {
|
|
56
|
+
it("creates four nav items: Collections, Archive, RSS, Settings", async () => {
|
|
84
57
|
await runSetupSeed(services);
|
|
85
58
|
|
|
86
|
-
const aboutPage = await services.pages.getBySlug("about");
|
|
87
59
|
const navItemsList = await services.navItems.list();
|
|
88
|
-
|
|
89
|
-
const aboutNavItem = navItemsList.find(
|
|
90
|
-
(item) => item.pageId === aboutPage?.id,
|
|
91
|
-
);
|
|
92
|
-
expect(aboutNavItem).toBeDefined();
|
|
93
|
-
expect(aboutNavItem?.type).toBe("page");
|
|
94
|
-
expect(aboutNavItem?.label).toBe("About");
|
|
95
|
-
expect(aboutNavItem?.url).toBe("/about");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("creates three nav items total: Collections, Archive, About", async () => {
|
|
99
|
-
await runSetupSeed(services);
|
|
100
|
-
|
|
101
|
-
const navItemsList = await services.navItems.list();
|
|
102
|
-
expect(navItemsList).toHaveLength(3);
|
|
60
|
+
expect(navItemsList).toHaveLength(4);
|
|
103
61
|
|
|
104
62
|
const labels = navItemsList.map((item) => item.label);
|
|
105
63
|
expect(labels).toContain("Collections");
|
|
106
64
|
expect(labels).toContain("Archive");
|
|
107
|
-
expect(labels).toContain("
|
|
65
|
+
expect(labels).toContain("RSS");
|
|
66
|
+
expect(labels).toContain("Settings");
|
|
108
67
|
});
|
|
109
68
|
|
|
110
|
-
it("
|
|
69
|
+
it("creates link and system type nav items", async () => {
|
|
111
70
|
await runSetupSeed(services);
|
|
112
71
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
72
|
+
const navItemsList = await services.navItems.list();
|
|
73
|
+
const linkItems = navItemsList.filter((item) => item.type === "link");
|
|
74
|
+
const systemItems = navItemsList.filter((item) => item.type === "system");
|
|
75
|
+
|
|
76
|
+
expect(linkItems).toHaveLength(2);
|
|
77
|
+
expect(systemItems).toHaveLength(2);
|
|
118
78
|
});
|
|
119
79
|
});
|
|
@@ -37,7 +37,7 @@ const ResetContent: FC<{ token: string }> = ({ token }) => {
|
|
|
37
37
|
</h2>
|
|
38
38
|
<p>
|
|
39
39
|
{t({
|
|
40
|
-
message: "
|
|
40
|
+
message: "Choose a new password.",
|
|
41
41
|
comment: "@context: Password reset page description",
|
|
42
42
|
})}
|
|
43
43
|
</p>
|
|
@@ -118,7 +118,7 @@ const ResetErrorContent: FC = () => {
|
|
|
118
118
|
<header>
|
|
119
119
|
<h2>
|
|
120
120
|
{t({
|
|
121
|
-
message: "
|
|
121
|
+
message: "This Link Has Expired",
|
|
122
122
|
comment: "@context: Password reset error heading",
|
|
123
123
|
})}
|
|
124
124
|
</h2>
|
|
@@ -127,7 +127,7 @@ const ResetErrorContent: FC = () => {
|
|
|
127
127
|
<p class="text-muted-foreground">
|
|
128
128
|
{t({
|
|
129
129
|
message:
|
|
130
|
-
"This
|
|
130
|
+
"This reset link is no longer valid. Request a new one to continue.",
|
|
131
131
|
comment: "@context: Password reset error description",
|
|
132
132
|
})}
|
|
133
133
|
</p>
|
|
@@ -175,7 +175,8 @@ resetRoutes.post("/reset", async (c) => {
|
|
|
175
175
|
parsed.error.issues[0]?.message ??
|
|
176
176
|
i18n._(
|
|
177
177
|
msg({
|
|
178
|
-
message:
|
|
178
|
+
message:
|
|
179
|
+
"Something doesn't look right. Check the form and try again.",
|
|
179
180
|
comment:
|
|
180
181
|
"@context: Fallback validation error for password reset form",
|
|
181
182
|
}),
|