@jant/core 0.3.26 → 0.3.28
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/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +21 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +112 -173
- package/src/auth.ts +4 -1
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -265
- package/dist/auth.js +0 -36
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
package/src/routes/api/pages.ts
CHANGED
|
@@ -3,25 +3,23 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
+
import { z } from "zod";
|
|
6
7
|
import type { Bindings } from "../../types.js";
|
|
7
|
-
import type { AppVariables } from "../../app.js";
|
|
8
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
8
9
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
10
|
+
import {
|
|
11
|
+
CreatePageSchema,
|
|
12
|
+
StatusSchema,
|
|
13
|
+
parseValidated,
|
|
14
|
+
} from "../../lib/schemas.js";
|
|
15
|
+
import { assertFound, parseIntParam, NotFoundError } from "../../lib/errors.js";
|
|
11
16
|
|
|
12
17
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
18
|
|
|
14
19
|
export const pagesApiRoutes = new Hono<Env>();
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
title: z.string().optional(),
|
|
19
|
-
body: z.string().optional(),
|
|
20
|
-
status: StatusSchema.optional(),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const UpdatePageSchema = z.object({
|
|
24
|
-
slug: z.string().min(1).optional(),
|
|
21
|
+
// API update schema extends shared schema with nullable fields for explicit clearing
|
|
22
|
+
const UpdatePageSchema = CreatePageSchema.partial().extend({
|
|
25
23
|
title: z.string().nullable().optional(),
|
|
26
24
|
body: z.string().nullable().optional(),
|
|
27
25
|
status: StatusSchema.optional(),
|
|
@@ -35,28 +33,14 @@ pagesApiRoutes.get("/", async (c) => {
|
|
|
35
33
|
|
|
36
34
|
// Get single page
|
|
37
35
|
pagesApiRoutes.get("/:id", async (c) => {
|
|
38
|
-
const id =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const page = await c.var.services.pages.getById(id);
|
|
42
|
-
if (!page) return c.json({ error: "Not found" }, 404);
|
|
43
|
-
|
|
36
|
+
const id = parseIntParam(c.req.param("id"));
|
|
37
|
+
const page = assertFound(await c.var.services.pages.getById(id), "Page");
|
|
44
38
|
return c.json(page);
|
|
45
39
|
});
|
|
46
40
|
|
|
47
41
|
// Create page (requires auth)
|
|
48
42
|
pagesApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const parseResult = CreatePageSchema.safeParse(rawBody);
|
|
52
|
-
if (!parseResult.success) {
|
|
53
|
-
return c.json(
|
|
54
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
55
|
-
400,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const body = parseResult.data;
|
|
43
|
+
const body = parseValidated(CreatePageSchema, await c.req.json());
|
|
60
44
|
|
|
61
45
|
const page = await c.var.services.pages.create({
|
|
62
46
|
slug: body.slug,
|
|
@@ -70,32 +54,20 @@ pagesApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
70
54
|
|
|
71
55
|
// Update page (requires auth)
|
|
72
56
|
pagesApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
73
|
-
const id =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const rawBody = await c.req.json();
|
|
77
|
-
|
|
78
|
-
const parseResult = UpdatePageSchema.safeParse(rawBody);
|
|
79
|
-
if (!parseResult.success) {
|
|
80
|
-
return c.json(
|
|
81
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
82
|
-
400,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
57
|
+
const id = parseIntParam(c.req.param("id"));
|
|
58
|
+
const body = parseValidated(UpdatePageSchema, await c.req.json());
|
|
85
59
|
|
|
86
|
-
const page = await c.var.services.pages.update(id,
|
|
87
|
-
if (!page) return c.json({ error: "Not found" }, 404);
|
|
60
|
+
const page = assertFound(await c.var.services.pages.update(id, body), "Page");
|
|
88
61
|
|
|
89
62
|
return c.json(page);
|
|
90
63
|
});
|
|
91
64
|
|
|
92
65
|
// Delete page (requires auth)
|
|
93
66
|
pagesApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
94
|
-
const id =
|
|
95
|
-
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
67
|
+
const id = parseIntParam(c.req.param("id"));
|
|
96
68
|
|
|
97
69
|
const success = await c.var.services.pages.delete(id);
|
|
98
|
-
if (!success)
|
|
70
|
+
if (!success) throw new NotFoundError("Page");
|
|
99
71
|
|
|
100
72
|
return c.json({ success: true });
|
|
101
73
|
});
|
package/src/routes/api/posts.ts
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings, Format, Status, Media } from "../../types.js";
|
|
7
|
-
import type { AppVariables } from "../../app.js";
|
|
7
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
8
8
|
import * as sqid from "../../lib/sqid.js";
|
|
9
9
|
import {
|
|
10
10
|
CreatePostSchema,
|
|
11
11
|
UpdatePostSchema,
|
|
12
12
|
validateMediaCount,
|
|
13
|
+
parseValidated,
|
|
13
14
|
} from "../../lib/schemas.js";
|
|
14
15
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
15
16
|
import {
|
|
@@ -17,6 +18,11 @@ import {
|
|
|
17
18
|
getImageUrl,
|
|
18
19
|
getPublicUrlForProvider,
|
|
19
20
|
} from "../../lib/image.js";
|
|
21
|
+
import {
|
|
22
|
+
assertFound,
|
|
23
|
+
NotFoundError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
} from "../../lib/errors.js";
|
|
20
26
|
|
|
21
27
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
28
|
|
|
@@ -57,6 +63,24 @@ function toMediaAttachment(
|
|
|
57
63
|
};
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Validates media IDs: checks count limit and verifies all IDs exist.
|
|
68
|
+
*/
|
|
69
|
+
async function validateMediaIds(
|
|
70
|
+
mediaIds: string[],
|
|
71
|
+
getByIds: (ids: string[]) => Promise<Media[]>,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
const countError = validateMediaCount(mediaIds);
|
|
74
|
+
if (countError) throw new ValidationError(countError);
|
|
75
|
+
|
|
76
|
+
if (mediaIds.length > 0) {
|
|
77
|
+
const existing = await getByIds(mediaIds);
|
|
78
|
+
if (existing.length !== mediaIds.length) {
|
|
79
|
+
throw new ValidationError("One or more media IDs are invalid");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
60
84
|
// List posts
|
|
61
85
|
postsApiRoutes.get("/", async (c) => {
|
|
62
86
|
const format = c.req.query("format") as Format | undefined;
|
|
@@ -74,9 +98,7 @@ postsApiRoutes.get("/", async (c) => {
|
|
|
74
98
|
// Batch load media for all posts
|
|
75
99
|
const postIds = posts.map((p) => p.id);
|
|
76
100
|
const mediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
77
|
-
const r2PublicUrl = c.
|
|
78
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
79
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
101
|
+
const { r2PublicUrl, imageTransformUrl, s3PublicUrl } = c.var.appConfig;
|
|
80
102
|
|
|
81
103
|
return c.json({
|
|
82
104
|
posts: posts.map((p) => ({
|
|
@@ -97,15 +119,12 @@ postsApiRoutes.get("/", async (c) => {
|
|
|
97
119
|
// Get single post
|
|
98
120
|
postsApiRoutes.get("/:id", async (c) => {
|
|
99
121
|
const id = sqid.decode(c.req.param("id"));
|
|
100
|
-
if (!id)
|
|
122
|
+
if (!id) throw new ValidationError("Invalid ID");
|
|
101
123
|
|
|
102
|
-
const post = await c.var.services.posts.getById(id);
|
|
103
|
-
if (!post) return c.json({ error: "Not found" }, 404);
|
|
124
|
+
const post = assertFound(await c.var.services.posts.getById(id), "Post");
|
|
104
125
|
|
|
105
126
|
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
106
|
-
const r2PublicUrl = c.
|
|
107
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
108
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
127
|
+
const { r2PublicUrl, imageTransformUrl, s3PublicUrl } = c.var.appConfig;
|
|
109
128
|
|
|
110
129
|
return c.json({
|
|
111
130
|
...post,
|
|
@@ -118,33 +137,13 @@ postsApiRoutes.get("/:id", async (c) => {
|
|
|
118
137
|
|
|
119
138
|
// Create post (requires auth)
|
|
120
139
|
postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
// Validate request body
|
|
124
|
-
const parseResult = CreatePostSchema.safeParse(rawBody);
|
|
125
|
-
if (!parseResult.success) {
|
|
126
|
-
return c.json(
|
|
127
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
128
|
-
400,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
140
|
+
const body = parseValidated(CreatePostSchema, await c.req.json());
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Validate media count
|
|
142
|
+
// Validate media IDs
|
|
135
143
|
if (body.mediaIds) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Verify all media IDs exist
|
|
142
|
-
if (body.mediaIds.length > 0) {
|
|
143
|
-
const existing = await c.var.services.media.getByIds(body.mediaIds);
|
|
144
|
-
if (existing.length !== body.mediaIds.length) {
|
|
145
|
-
return c.json({ error: "One or more media IDs are invalid" }, 400);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
144
|
+
await validateMediaIds(body.mediaIds, (ids) =>
|
|
145
|
+
c.var.services.media.getByIds(ids),
|
|
146
|
+
);
|
|
148
147
|
}
|
|
149
148
|
|
|
150
149
|
const post = await c.var.services.posts.create({
|
|
@@ -158,7 +157,7 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
158
157
|
url: body.url || undefined,
|
|
159
158
|
quoteText: body.quoteText,
|
|
160
159
|
rating: body.rating || undefined,
|
|
161
|
-
|
|
160
|
+
collectionIds: body.collectionIds?.length ? body.collectionIds : undefined,
|
|
162
161
|
replyToId: body.replyToId
|
|
163
162
|
? (sqid.decode(body.replyToId) ?? undefined)
|
|
164
163
|
: undefined,
|
|
@@ -171,9 +170,7 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
171
170
|
}
|
|
172
171
|
|
|
173
172
|
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
174
|
-
const r2PublicUrl = c.
|
|
175
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
176
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
173
|
+
const { r2PublicUrl, imageTransformUrl, s3PublicUrl } = c.var.appConfig;
|
|
177
174
|
|
|
178
175
|
return c.json(
|
|
179
176
|
{
|
|
@@ -190,53 +187,36 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
190
187
|
// Update post (requires auth)
|
|
191
188
|
postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
192
189
|
const id = sqid.decode(c.req.param("id"));
|
|
193
|
-
if (!id)
|
|
194
|
-
|
|
195
|
-
const rawBody = await c.req.json();
|
|
196
|
-
|
|
197
|
-
// Validate request body
|
|
198
|
-
const parseResult = UpdatePostSchema.safeParse(rawBody);
|
|
199
|
-
if (!parseResult.success) {
|
|
200
|
-
return c.json(
|
|
201
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
202
|
-
400,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
190
|
+
if (!id) throw new ValidationError("Invalid ID");
|
|
205
191
|
|
|
206
|
-
const body =
|
|
192
|
+
const body = parseValidated(UpdatePostSchema, await c.req.json());
|
|
207
193
|
|
|
208
|
-
// Validate media
|
|
194
|
+
// Validate media IDs if provided
|
|
209
195
|
if (body.mediaIds !== undefined) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Verify all media IDs exist
|
|
216
|
-
if (body.mediaIds.length > 0) {
|
|
217
|
-
const existing = await c.var.services.media.getByIds(body.mediaIds);
|
|
218
|
-
if (existing.length !== body.mediaIds.length) {
|
|
219
|
-
return c.json({ error: "One or more media IDs are invalid" }, 400);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
196
|
+
await validateMediaIds(body.mediaIds, (ids) =>
|
|
197
|
+
c.var.services.media.getByIds(ids),
|
|
198
|
+
);
|
|
222
199
|
}
|
|
223
200
|
|
|
224
|
-
const post =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
201
|
+
const post = assertFound(
|
|
202
|
+
await c.var.services.posts.update(id, {
|
|
203
|
+
format: body.format,
|
|
204
|
+
title: body.title,
|
|
205
|
+
body: body.body,
|
|
206
|
+
path: body.path,
|
|
207
|
+
status: body.status,
|
|
208
|
+
featured: body.featured,
|
|
209
|
+
pinned: body.pinned,
|
|
210
|
+
url: body.url,
|
|
211
|
+
quoteText: body.quoteText,
|
|
212
|
+
rating: body.rating || undefined,
|
|
213
|
+
collectionIds: body.collectionIds?.length
|
|
214
|
+
? body.collectionIds
|
|
215
|
+
: undefined,
|
|
216
|
+
publishedAt: body.publishedAt,
|
|
217
|
+
}),
|
|
218
|
+
"Post",
|
|
219
|
+
);
|
|
240
220
|
|
|
241
221
|
// Update media attachments if provided (including empty array to clear)
|
|
242
222
|
if (body.mediaIds !== undefined) {
|
|
@@ -244,9 +224,7 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
244
224
|
}
|
|
245
225
|
|
|
246
226
|
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
247
|
-
const r2PublicUrl = c.
|
|
248
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
249
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
227
|
+
const { r2PublicUrl, imageTransformUrl, s3PublicUrl } = c.var.appConfig;
|
|
250
228
|
|
|
251
229
|
return c.json({
|
|
252
230
|
...post,
|
|
@@ -260,13 +238,13 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
260
238
|
// Delete post (requires auth)
|
|
261
239
|
postsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
262
240
|
const id = sqid.decode(c.req.param("id"));
|
|
263
|
-
if (!id)
|
|
241
|
+
if (!id) throw new ValidationError("Invalid ID");
|
|
264
242
|
|
|
265
243
|
// Detach media before deleting
|
|
266
244
|
await c.var.services.media.detachFromPost(id);
|
|
267
245
|
|
|
268
246
|
const success = await c.var.services.posts.delete(id);
|
|
269
|
-
if (!success)
|
|
247
|
+
if (!success) throw new NotFoundError("Post");
|
|
270
248
|
|
|
271
249
|
return c.json({ success: true });
|
|
272
250
|
});
|
package/src/routes/api/search.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
|
-
import type { AppVariables } from "../../app.js";
|
|
7
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
8
8
|
import * as sqid from "../../lib/sqid.js";
|
|
9
|
+
import { ValidationError, ExternalServiceError } from "../../lib/errors.js";
|
|
9
10
|
|
|
10
11
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
11
12
|
|
|
@@ -16,11 +17,11 @@ searchApiRoutes.get("/", async (c) => {
|
|
|
16
17
|
const query = c.req.query("q");
|
|
17
18
|
|
|
18
19
|
if (!query || query.trim().length === 0) {
|
|
19
|
-
|
|
20
|
+
throw new ValidationError("Query parameter 'q' is required");
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
if (query.length > 200) {
|
|
23
|
-
|
|
24
|
+
throw new ValidationError("Query too long");
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const limitParam = c.req.query("limit");
|
|
@@ -46,8 +47,9 @@ searchApiRoutes.get("/", async (c) => {
|
|
|
46
47
|
count: results.length,
|
|
47
48
|
});
|
|
48
49
|
} catch (err) {
|
|
50
|
+
if (err instanceof ValidationError) throw err;
|
|
49
51
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
50
52
|
console.error("Search error:", err);
|
|
51
|
-
|
|
53
|
+
throw new ExternalServiceError("Search failed");
|
|
52
54
|
}
|
|
53
55
|
});
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
|
-
import type { AppVariables } from "../../app.js";
|
|
7
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
8
8
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
9
|
import { CONFIG_FIELDS, type ConfigKey } from "../../types.js";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
+
import { parseValidated } from "../../lib/schemas.js";
|
|
12
|
+
import { ValidationError } from "../../lib/errors.js";
|
|
11
13
|
|
|
12
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
15
|
|
|
@@ -35,17 +37,7 @@ settingsApiRoutes.get("/", requireAuthApi(), async (c) => {
|
|
|
35
37
|
|
|
36
38
|
// Update settings (requires auth)
|
|
37
39
|
settingsApiRoutes.put("/", requireAuthApi(), async (c) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const parseResult = UpdateSettingsSchema.safeParse(rawBody);
|
|
41
|
-
if (!parseResult.success) {
|
|
42
|
-
return c.json(
|
|
43
|
-
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
44
|
-
400,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const updates = parseResult.data;
|
|
40
|
+
const updates = parseValidated(UpdateSettingsSchema, await c.req.json());
|
|
49
41
|
|
|
50
42
|
// Filter to only editable keys
|
|
51
43
|
const filteredUpdates: Partial<Record<ConfigKey, string>> = {};
|
|
@@ -60,21 +52,13 @@ settingsApiRoutes.put("/", requireAuthApi(), async (c) => {
|
|
|
60
52
|
}
|
|
61
53
|
|
|
62
54
|
if (rejectedKeys.length > 0 && Object.keys(filteredUpdates).length === 0) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
rejectedKeys,
|
|
67
|
-
},
|
|
68
|
-
400,
|
|
69
|
-
);
|
|
55
|
+
throw new ValidationError("None of the provided keys are editable", {
|
|
56
|
+
rejectedKeys,
|
|
57
|
+
});
|
|
70
58
|
}
|
|
71
59
|
|
|
72
60
|
if (Object.keys(filteredUpdates).length > 0) {
|
|
73
|
-
|
|
74
|
-
// editable (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE) are valid SettingsKeys
|
|
75
|
-
for (const [key, value] of Object.entries(filteredUpdates)) {
|
|
76
|
-
await c.var.services.settings.set(key as never, value as string);
|
|
77
|
-
}
|
|
61
|
+
await c.var.services.settings.setMany(filteredUpdates as never);
|
|
78
62
|
}
|
|
79
63
|
|
|
80
64
|
// Return updated state
|
package/src/routes/api/upload.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { Hono, type Context } from "hono";
|
|
9
9
|
import { html } from "hono/html";
|
|
10
|
-
import {
|
|
10
|
+
import { msg } from "@lingui/core/macro";
|
|
11
11
|
import type { Bindings } from "../../types.js";
|
|
12
|
-
import type { AppVariables } from "../../app.js";
|
|
12
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
13
13
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
14
14
|
import {
|
|
15
15
|
getMediaUrl,
|
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
getPublicUrlForProvider,
|
|
18
18
|
} from "../../lib/image.js";
|
|
19
19
|
import { sse } from "../../lib/sse.js";
|
|
20
|
+
import { validateUploadFile, generateStorageKey } from "../../lib/upload.js";
|
|
21
|
+
import { assertFound } from "../../lib/errors.js";
|
|
22
|
+
import { getI18n } from "../../i18n/index.js";
|
|
20
23
|
|
|
21
24
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
25
|
|
|
@@ -130,56 +133,48 @@ function sseUploadError(c: Context<Env>, message: string): Response {
|
|
|
130
133
|
|
|
131
134
|
// Upload a file
|
|
132
135
|
uploadApiRoutes.post("/", async (c) => {
|
|
136
|
+
const i18n = getI18n(c);
|
|
133
137
|
const storage = c.var.storage;
|
|
134
138
|
if (!storage) {
|
|
139
|
+
const errorText = i18n._(
|
|
140
|
+
msg({
|
|
141
|
+
message: "Storage not configured",
|
|
142
|
+
comment: "@context: Error when file storage is not set up",
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
135
145
|
if (wantsSSE(c)) {
|
|
136
|
-
return sseUploadError(c,
|
|
146
|
+
return sseUploadError(c, errorText);
|
|
137
147
|
}
|
|
138
|
-
return c.json({ error:
|
|
148
|
+
return c.json({ error: errorText }, 500);
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
const formData = await c.req.formData();
|
|
142
152
|
const file = formData.get("file") as File | null;
|
|
143
153
|
|
|
144
154
|
if (!file) {
|
|
155
|
+
const errorText = i18n._(
|
|
156
|
+
msg({
|
|
157
|
+
message: "No file provided",
|
|
158
|
+
comment: "@context: Error when no file was selected for upload",
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
145
161
|
if (wantsSSE(c)) {
|
|
146
|
-
return sseUploadError(c,
|
|
147
|
-
}
|
|
148
|
-
return c.json({ error: "No file provided" }, 400);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Validate file type
|
|
152
|
-
const allowedTypes = [
|
|
153
|
-
"image/jpeg",
|
|
154
|
-
"image/png",
|
|
155
|
-
"image/gif",
|
|
156
|
-
"image/webp",
|
|
157
|
-
"image/svg+xml",
|
|
158
|
-
];
|
|
159
|
-
if (!allowedTypes.includes(file.type)) {
|
|
160
|
-
if (wantsSSE(c)) {
|
|
161
|
-
return sseUploadError(c, "File type not allowed");
|
|
162
|
+
return sseUploadError(c, errorText);
|
|
162
163
|
}
|
|
163
|
-
return c.json({ error:
|
|
164
|
+
return c.json({ error: errorText }, 400);
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
// Validate file
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
167
|
+
// Validate file type and size
|
|
168
|
+
const uploadError = validateUploadFile(file);
|
|
169
|
+
if (uploadError) {
|
|
169
170
|
if (wantsSSE(c)) {
|
|
170
|
-
return sseUploadError(c,
|
|
171
|
+
return sseUploadError(c, uploadError);
|
|
171
172
|
}
|
|
172
|
-
return c.json({ error:
|
|
173
|
+
return c.json({ error: uploadError }, 400);
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
// Generate unique filename using UUIDv7
|
|
176
|
-
const
|
|
177
|
-
const id = uuidv7();
|
|
178
|
-
const date = new Date();
|
|
179
|
-
const year = date.getUTCFullYear();
|
|
180
|
-
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
181
|
-
const filename = `${id}.${ext}`;
|
|
182
|
-
const storageKey = `media/${year}/${month}/${filename}`;
|
|
177
|
+
const { id, filename, storageKey } = generateStorageKey(file.name);
|
|
183
178
|
|
|
184
179
|
try {
|
|
185
180
|
// Upload to storage
|
|
@@ -195,21 +190,20 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
195
190
|
mimeType: file.type,
|
|
196
191
|
size: file.size,
|
|
197
192
|
storageKey,
|
|
198
|
-
provider: c.
|
|
193
|
+
provider: c.var.appConfig.storageDriver,
|
|
199
194
|
});
|
|
200
195
|
|
|
201
196
|
// SSE response for Datastar
|
|
202
197
|
if (wantsSSE(c)) {
|
|
203
|
-
const provider = c.env.STORAGE_DRIVER || "r2";
|
|
204
198
|
const mediaPublicUrl = getPublicUrlForProvider(
|
|
205
|
-
|
|
206
|
-
c.
|
|
207
|
-
c.
|
|
199
|
+
c.var.appConfig.storageDriver,
|
|
200
|
+
c.var.appConfig.r2PublicUrl,
|
|
201
|
+
c.var.appConfig.s3PublicUrl,
|
|
208
202
|
);
|
|
209
203
|
const cardHtml = renderMediaCard(
|
|
210
204
|
media,
|
|
211
205
|
mediaPublicUrl,
|
|
212
|
-
c.
|
|
206
|
+
c.var.appConfig.imageTransformUrl,
|
|
213
207
|
);
|
|
214
208
|
|
|
215
209
|
return sse(c, async (stream) => {
|
|
@@ -218,16 +212,22 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
218
212
|
mode: "outer",
|
|
219
213
|
selector: "#upload-placeholder",
|
|
220
214
|
});
|
|
221
|
-
await stream.toast(
|
|
215
|
+
await stream.toast(
|
|
216
|
+
i18n._(
|
|
217
|
+
msg({
|
|
218
|
+
message: "Upload successful!",
|
|
219
|
+
comment: "@context: Toast after successful file upload",
|
|
220
|
+
}),
|
|
221
|
+
),
|
|
222
|
+
);
|
|
222
223
|
});
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
// JSON response for API clients
|
|
226
|
-
const provider = c.env.STORAGE_DRIVER || "r2";
|
|
227
227
|
const mediaPublicUrl = getPublicUrlForProvider(
|
|
228
|
-
|
|
229
|
-
c.
|
|
230
|
-
c.
|
|
228
|
+
c.var.appConfig.storageDriver,
|
|
229
|
+
c.var.appConfig.r2PublicUrl,
|
|
230
|
+
c.var.appConfig.s3PublicUrl,
|
|
231
231
|
);
|
|
232
232
|
const publicUrl = getMediaUrl(storageKey, mediaPublicUrl);
|
|
233
233
|
return c.json({
|
|
@@ -241,22 +241,27 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
241
241
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
242
242
|
console.error("Upload error:", err);
|
|
243
243
|
|
|
244
|
+
const errorText = i18n._(
|
|
245
|
+
msg({
|
|
246
|
+
message: "Upload failed. Please try again.",
|
|
247
|
+
comment: "@context: Error when file upload fails",
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
244
250
|
if (wantsSSE(c)) {
|
|
245
251
|
return sse(c, async (stream) => {
|
|
246
252
|
await stream.remove("#upload-placeholder");
|
|
247
|
-
await stream.toast(
|
|
253
|
+
await stream.toast(errorText, "error");
|
|
248
254
|
});
|
|
249
255
|
}
|
|
250
|
-
return c.json({ error:
|
|
256
|
+
return c.json({ error: errorText }, 500);
|
|
251
257
|
}
|
|
252
258
|
});
|
|
253
259
|
|
|
254
260
|
// List uploaded files (JSON only)
|
|
255
261
|
uploadApiRoutes.get("/", async (c) => {
|
|
256
262
|
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
257
|
-
const mediaList = await c.var.services.media.list(limit);
|
|
258
|
-
const r2PublicUrl = c.
|
|
259
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
263
|
+
const mediaList = await c.var.services.media.list({ limit });
|
|
264
|
+
const { r2PublicUrl, s3PublicUrl } = c.var.appConfig;
|
|
260
265
|
|
|
261
266
|
return c.json({
|
|
262
267
|
media: mediaList.map((m) => ({
|
|
@@ -276,10 +281,7 @@ uploadApiRoutes.get("/", async (c) => {
|
|
|
276
281
|
// Delete a file
|
|
277
282
|
uploadApiRoutes.delete("/:id", async (c) => {
|
|
278
283
|
const id = c.req.param("id");
|
|
279
|
-
const media = await c.var.services.media.getById(id);
|
|
280
|
-
if (!media) {
|
|
281
|
-
return c.json({ error: "Not found" }, 404);
|
|
282
|
-
}
|
|
284
|
+
const media = assertFound(await c.var.services.media.getById(id), "Media");
|
|
283
285
|
|
|
284
286
|
// Delete from storage
|
|
285
287
|
const storage = c.var.storage;
|