@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
|
@@ -1,64 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Settings Routes
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Unified settings hub — root page with iOS-style grouped list,
|
|
5
|
+
* plus sub-pages for General, Avatar, Navigation, Color Theme,
|
|
6
|
+
* Font Theme, Custom CSS, Account (Sessions + Password), and API Tokens.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { Hono } from "hono";
|
|
8
10
|
import { msg } from "@lingui/core/macro";
|
|
9
11
|
import type { Bindings } from "../../types.js";
|
|
10
12
|
import type { AppVariables } from "../../types/app-context.js";
|
|
11
|
-
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
12
13
|
import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
|
|
13
14
|
import { getI18n } from "../../i18n/index.js";
|
|
15
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
16
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
17
|
+
import { AdminBreadcrumb } from "../../ui/shared/AdminBreadcrumb.js";
|
|
14
18
|
import { TIMEZONES } from "../../lib/timezones.js";
|
|
15
19
|
import { escapeHtml } from "../../lib/html.js";
|
|
16
20
|
import { ValidationError } from "../../lib/errors.js";
|
|
21
|
+
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
22
|
+
import { getAvailableThemes } from "../../lib/theme.js";
|
|
23
|
+
import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
|
|
24
|
+
import { SettingsRootContent } from "../../ui/dash/settings/SettingsRootContent.js";
|
|
17
25
|
import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
|
|
26
|
+
import { AvatarContent } from "../../ui/dash/settings/AvatarContent.js";
|
|
27
|
+
import { AccountMenuContent } from "../../ui/dash/settings/AccountMenuContent.js";
|
|
18
28
|
import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
|
|
29
|
+
import {
|
|
30
|
+
SessionsContent,
|
|
31
|
+
type SessionInfo,
|
|
32
|
+
} from "../../ui/dash/settings/SessionsContent.js";
|
|
33
|
+
import { NavigationContent } from "../../ui/dash/appearance/NavigationContent.js";
|
|
34
|
+
import { ColorThemeContent } from "../../ui/dash/appearance/ColorThemeContent.js";
|
|
35
|
+
import { FontThemeContent } from "../../ui/dash/appearance/FontThemeContent.js";
|
|
36
|
+
import { AdvancedContent } from "../../ui/dash/appearance/AdvancedContent.js";
|
|
37
|
+
import { ApiTokensContent } from "../../ui/dash/settings/ApiTokensContent.js";
|
|
19
38
|
|
|
20
39
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
21
40
|
|
|
22
41
|
export const settingsRoutes = new Hono<Env>();
|
|
23
42
|
|
|
24
43
|
// ===========================================================================
|
|
25
|
-
//
|
|
44
|
+
// Settings root — iOS-style grouped list
|
|
26
45
|
// ===========================================================================
|
|
27
46
|
|
|
28
47
|
settingsRoutes.get("/", async (c) => {
|
|
48
|
+
const navData = await getNavigationData(c);
|
|
49
|
+
|
|
50
|
+
return renderPublicPage(c, {
|
|
51
|
+
title: `Settings - ${navData.siteName}`,
|
|
52
|
+
navData,
|
|
53
|
+
content: <SettingsRootContent />,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ===========================================================================
|
|
58
|
+
// General settings
|
|
59
|
+
// ===========================================================================
|
|
60
|
+
|
|
61
|
+
settingsRoutes.get("/general", async (c) => {
|
|
29
62
|
const { allSettings, appConfig } = c.var;
|
|
30
63
|
|
|
31
64
|
const dbSiteName = allSettings["SITE_NAME"] ?? "";
|
|
32
65
|
const dbSiteDescription = allSettings["SITE_DESCRIPTION"] ?? "";
|
|
33
66
|
|
|
34
67
|
const saved = c.req.query("saved") !== undefined;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
const navData = await getNavigationData(c);
|
|
69
|
+
|
|
70
|
+
return renderPublicPage(c, {
|
|
71
|
+
title: `General - ${navData.siteName}`,
|
|
72
|
+
navData,
|
|
73
|
+
toast: saved ? { message: "Settings updated." } : undefined,
|
|
74
|
+
content: (
|
|
75
|
+
<>
|
|
76
|
+
<AdminBreadcrumb
|
|
77
|
+
parent="Settings"
|
|
78
|
+
parentHref="/settings"
|
|
79
|
+
current="General"
|
|
80
|
+
/>
|
|
81
|
+
<GeneralContent
|
|
82
|
+
siteName={dbSiteName || ""}
|
|
83
|
+
siteDescription={dbSiteDescription || ""}
|
|
84
|
+
siteLanguage={appConfig.siteLanguage}
|
|
85
|
+
siteNameFallback={appConfig.fallbacks.siteName}
|
|
86
|
+
siteDescriptionFallback={appConfig.fallbacks.siteDescription}
|
|
87
|
+
timeZone={appConfig.timeZone}
|
|
88
|
+
siteFooter={appConfig.siteFooter}
|
|
89
|
+
noindex={appConfig.noindex}
|
|
90
|
+
timezones={TIMEZONES}
|
|
91
|
+
/>
|
|
92
|
+
</>
|
|
93
|
+
),
|
|
94
|
+
});
|
|
59
95
|
});
|
|
60
96
|
|
|
61
|
-
settingsRoutes.post("/", async (c) => {
|
|
97
|
+
settingsRoutes.post("/general", async (c) => {
|
|
62
98
|
const i18n = getI18n(c);
|
|
63
99
|
const body = await c.req.json<{
|
|
64
100
|
siteName: string;
|
|
@@ -76,43 +112,36 @@ settingsRoutes.post("/", async (c) => {
|
|
|
76
112
|
fallbackSiteName: c.var.appConfig.fallbacks.siteName,
|
|
77
113
|
});
|
|
78
114
|
|
|
115
|
+
// Sync user.name with site name (better-auth requires this field)
|
|
116
|
+
await c.var.auth.api.updateUser({
|
|
117
|
+
body: { name: displayName },
|
|
118
|
+
headers: c.req.raw.headers,
|
|
119
|
+
});
|
|
120
|
+
|
|
79
121
|
// ── JSON response mode (used by Lit settings bridge) ──────────────
|
|
122
|
+
// Always redirect — site name appears in the header/title and a full
|
|
123
|
+
// reload is the simplest way to keep everything in sync.
|
|
80
124
|
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
81
125
|
if (wantsJson) {
|
|
82
|
-
if (languageChanged) {
|
|
83
|
-
return c.json({
|
|
84
|
-
status: "redirect" as const,
|
|
85
|
-
url: "/dash/settings?saved",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
126
|
return c.json({
|
|
89
|
-
status: "
|
|
90
|
-
|
|
91
|
-
msg({
|
|
92
|
-
message: "Settings saved successfully.",
|
|
93
|
-
comment: "@context: Toast after saving general settings",
|
|
94
|
-
}),
|
|
95
|
-
),
|
|
96
|
-
siteName: displayName,
|
|
127
|
+
status: "redirect" as const,
|
|
128
|
+
url: "/settings/general?saved",
|
|
97
129
|
});
|
|
98
130
|
}
|
|
99
131
|
|
|
100
132
|
return sse(c, async (stream) => {
|
|
101
133
|
if (languageChanged) {
|
|
102
|
-
await stream.redirect("/
|
|
134
|
+
await stream.redirect("/settings/general?saved");
|
|
103
135
|
} else {
|
|
104
136
|
const escaped = escapeHtml(displayName);
|
|
105
|
-
await stream.patchElements(
|
|
106
|
-
`<a id="site-name" href="/dash" class="font-semibold">${escaped}</a>`,
|
|
107
|
-
);
|
|
108
|
-
await stream.patchElements(`Settings - ${escaped}`, {
|
|
137
|
+
await stream.patchElements(`General - ${escaped}`, {
|
|
109
138
|
mode: "inner",
|
|
110
139
|
selector: "title",
|
|
111
140
|
});
|
|
112
141
|
await stream.toast(
|
|
113
142
|
i18n._(
|
|
114
143
|
msg({
|
|
115
|
-
message: "Settings
|
|
144
|
+
message: "Settings updated.",
|
|
116
145
|
comment: "@context: Toast after saving general settings",
|
|
117
146
|
}),
|
|
118
147
|
),
|
|
@@ -129,7 +158,7 @@ settingsRoutes.post("/", async (c) => {
|
|
|
129
158
|
});
|
|
130
159
|
});
|
|
131
160
|
|
|
132
|
-
settingsRoutes.post("/seo", async (c) => {
|
|
161
|
+
settingsRoutes.post("/general/seo", async (c) => {
|
|
133
162
|
const i18n = getI18n(c);
|
|
134
163
|
const body = await c.req.json<{ noindex: string }>();
|
|
135
164
|
const { settings } = c.var.services;
|
|
@@ -150,7 +179,7 @@ settingsRoutes.post("/seo", async (c) => {
|
|
|
150
179
|
status: "ok" as const,
|
|
151
180
|
toast: i18n._(
|
|
152
181
|
msg({
|
|
153
|
-
message: "SEO settings
|
|
182
|
+
message: "SEO settings updated.",
|
|
154
183
|
comment: "@context: Toast after saving SEO settings",
|
|
155
184
|
}),
|
|
156
185
|
),
|
|
@@ -161,7 +190,7 @@ settingsRoutes.post("/seo", async (c) => {
|
|
|
161
190
|
await stream.toast(
|
|
162
191
|
i18n._(
|
|
163
192
|
msg({
|
|
164
|
-
message: "SEO settings
|
|
193
|
+
message: "SEO settings updated.",
|
|
165
194
|
comment: "@context: Toast after saving SEO settings",
|
|
166
195
|
}),
|
|
167
196
|
),
|
|
@@ -174,9 +203,33 @@ settingsRoutes.post("/seo", async (c) => {
|
|
|
174
203
|
});
|
|
175
204
|
|
|
176
205
|
// ===========================================================================
|
|
177
|
-
// Avatar
|
|
206
|
+
// Avatar
|
|
178
207
|
// ===========================================================================
|
|
179
208
|
|
|
209
|
+
settingsRoutes.get("/avatar", async (c) => {
|
|
210
|
+
const saved = c.req.query("saved") !== undefined;
|
|
211
|
+
const navData = await getNavigationData(c);
|
|
212
|
+
|
|
213
|
+
return renderPublicPage(c, {
|
|
214
|
+
title: `Avatar - ${navData.siteName}`,
|
|
215
|
+
navData,
|
|
216
|
+
toast: saved ? { message: "Avatar updated." } : undefined,
|
|
217
|
+
content: (
|
|
218
|
+
<>
|
|
219
|
+
<AdminBreadcrumb
|
|
220
|
+
parent="Settings"
|
|
221
|
+
parentHref="/settings"
|
|
222
|
+
current="Avatar"
|
|
223
|
+
/>
|
|
224
|
+
<AvatarContent
|
|
225
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
226
|
+
showHeaderAvatar={c.var.appConfig.showHeaderAvatar}
|
|
227
|
+
/>
|
|
228
|
+
</>
|
|
229
|
+
),
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
180
233
|
settingsRoutes.post("/avatar", async (c) => {
|
|
181
234
|
const i18n = getI18n(c);
|
|
182
235
|
const storage = c.var.storage;
|
|
@@ -184,7 +237,7 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
184
237
|
return dsToast(
|
|
185
238
|
i18n._(
|
|
186
239
|
msg({
|
|
187
|
-
message: "
|
|
240
|
+
message: "File storage isn't set up. Check your server config.",
|
|
188
241
|
comment: "@context: Error toast when file storage is not set up",
|
|
189
242
|
}),
|
|
190
243
|
),
|
|
@@ -198,7 +251,7 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
198
251
|
return dsToast(
|
|
199
252
|
i18n._(
|
|
200
253
|
msg({
|
|
201
|
-
message: "No file
|
|
254
|
+
message: "No file selected. Choose a file to upload.",
|
|
202
255
|
comment: "@context: Error toast when no file was selected for upload",
|
|
203
256
|
}),
|
|
204
257
|
),
|
|
@@ -222,10 +275,11 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
222
275
|
media: c.var.services.media,
|
|
223
276
|
storage,
|
|
224
277
|
storageProvider: c.var.appConfig.storageDriver,
|
|
278
|
+
maxFileSizeMB: c.var.appConfig.uploadMaxFileSize,
|
|
225
279
|
},
|
|
226
280
|
);
|
|
227
281
|
|
|
228
|
-
return dsRedirect("/
|
|
282
|
+
return dsRedirect("/settings/avatar?saved");
|
|
229
283
|
} catch (e) {
|
|
230
284
|
if (e instanceof ValidationError) {
|
|
231
285
|
return dsToast(e.message, "error");
|
|
@@ -233,7 +287,7 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
233
287
|
return dsToast(
|
|
234
288
|
i18n._(
|
|
235
289
|
msg({
|
|
236
|
-
message: "Upload
|
|
290
|
+
message: "Upload didn't go through. Try again in a moment.",
|
|
237
291
|
comment: "@context: Error toast when avatar upload fails",
|
|
238
292
|
}),
|
|
239
293
|
),
|
|
@@ -248,10 +302,13 @@ settingsRoutes.post("/avatar/remove", async (c) => {
|
|
|
248
302
|
// ── JSON response mode (used by Lit settings bridge) ──────────────
|
|
249
303
|
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
250
304
|
if (wantsJson) {
|
|
251
|
-
return c.json({
|
|
305
|
+
return c.json({
|
|
306
|
+
status: "redirect" as const,
|
|
307
|
+
url: "/settings/avatar?saved",
|
|
308
|
+
});
|
|
252
309
|
}
|
|
253
310
|
|
|
254
|
-
return dsRedirect("/
|
|
311
|
+
return dsRedirect("/settings/avatar?saved");
|
|
255
312
|
});
|
|
256
313
|
|
|
257
314
|
settingsRoutes.post("/avatar/display", async (c) => {
|
|
@@ -272,7 +329,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
|
|
|
272
329
|
status: "ok" as const,
|
|
273
330
|
toast: i18n._(
|
|
274
331
|
msg({
|
|
275
|
-
message: "Avatar display
|
|
332
|
+
message: "Avatar display updated.",
|
|
276
333
|
comment: "@context: Toast after saving avatar display preference",
|
|
277
334
|
}),
|
|
278
335
|
),
|
|
@@ -283,7 +340,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
|
|
|
283
340
|
await stream.toast(
|
|
284
341
|
i18n._(
|
|
285
342
|
msg({
|
|
286
|
-
message: "Avatar display
|
|
343
|
+
message: "Avatar display updated.",
|
|
287
344
|
comment: "@context: Toast after saving avatar display preference",
|
|
288
345
|
}),
|
|
289
346
|
),
|
|
@@ -296,75 +353,343 @@ settingsRoutes.post("/avatar/display", async (c) => {
|
|
|
296
353
|
});
|
|
297
354
|
|
|
298
355
|
// ===========================================================================
|
|
299
|
-
//
|
|
356
|
+
// Navigation (moved from appearance routes)
|
|
300
357
|
// ===========================================================================
|
|
301
358
|
|
|
302
|
-
settingsRoutes.get("/
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
|
|
359
|
+
settingsRoutes.get("/navigation", async (c) => {
|
|
360
|
+
const navItems = await c.var.services.navItems.list();
|
|
361
|
+
const headerNavMaxVisible = c.var.appConfig.headerNavMaxVisible;
|
|
362
|
+
const homeDefaultView = c.var.appConfig.homeDefaultView;
|
|
363
|
+
const navData = await getNavigationData(c);
|
|
364
|
+
|
|
365
|
+
return renderPublicPage(c, {
|
|
366
|
+
title: `Navigation - ${navData.siteName}`,
|
|
367
|
+
navData,
|
|
368
|
+
content: (
|
|
369
|
+
<>
|
|
370
|
+
<AdminBreadcrumb
|
|
371
|
+
parent="Settings"
|
|
372
|
+
parentHref="/settings"
|
|
373
|
+
current="Navigation"
|
|
374
|
+
/>
|
|
375
|
+
<NavigationContent
|
|
376
|
+
navItems={navItems}
|
|
377
|
+
headerNavMaxVisible={headerNavMaxVisible}
|
|
378
|
+
homeDefaultView={homeDefaultView}
|
|
379
|
+
siteName={navData.siteName}
|
|
380
|
+
/>
|
|
381
|
+
</>
|
|
382
|
+
),
|
|
306
383
|
});
|
|
307
|
-
|
|
308
|
-
const saved = c.req.query("saved") !== undefined;
|
|
384
|
+
});
|
|
309
385
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
386
|
+
settingsRoutes.post("/navigation/nav-max-visible", async (c) => {
|
|
387
|
+
const body = await c.req.json<{ value: number }>();
|
|
388
|
+
const { settings } = c.var.services;
|
|
389
|
+
|
|
390
|
+
const navMax = Math.max(0, Math.min(5, body.value ?? 3));
|
|
391
|
+
if (navMax !== 3) {
|
|
392
|
+
await settings.set("HEADER_NAV_MAX_VISIBLE", String(navMax));
|
|
393
|
+
} else {
|
|
394
|
+
await settings.remove("HEADER_NAV_MAX_VISIBLE");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return c.json({ ok: true });
|
|
321
398
|
});
|
|
322
399
|
|
|
323
|
-
settingsRoutes.post("/
|
|
400
|
+
settingsRoutes.post("/navigation/home-default-view", async (c) => {
|
|
401
|
+
const body = await c.req.json<{ value: string }>();
|
|
402
|
+
const { settings } = c.var.services;
|
|
403
|
+
|
|
404
|
+
if (body.value === "featured") {
|
|
405
|
+
await settings.set("HOME_DEFAULT_VIEW", "featured");
|
|
406
|
+
} else {
|
|
407
|
+
await settings.remove("HOME_DEFAULT_VIEW");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return c.json({ ok: true });
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ===========================================================================
|
|
414
|
+
// Color Theme (moved from appearance routes)
|
|
415
|
+
// ===========================================================================
|
|
416
|
+
|
|
417
|
+
settingsRoutes.get("/color-theme", async (c) => {
|
|
418
|
+
const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
|
|
419
|
+
const currentThemeId =
|
|
420
|
+
c.var.allSettings[SETTINGS_KEYS.THEME] ?? defaultThemeId;
|
|
421
|
+
const themes = getAvailableThemes();
|
|
422
|
+
const saved = c.req.query("saved") !== undefined;
|
|
423
|
+
const navData = await getNavigationData(c);
|
|
424
|
+
|
|
425
|
+
return renderPublicPage(c, {
|
|
426
|
+
title: `Color Theme - ${navData.siteName}`,
|
|
427
|
+
navData,
|
|
428
|
+
toast: saved ? { message: "Theme updated." } : undefined,
|
|
429
|
+
content: (
|
|
430
|
+
<>
|
|
431
|
+
<AdminBreadcrumb
|
|
432
|
+
parent="Settings"
|
|
433
|
+
parentHref="/settings"
|
|
434
|
+
current="Color Theme"
|
|
435
|
+
/>
|
|
436
|
+
<ColorThemeContent themes={themes} currentThemeId={currentThemeId} />
|
|
437
|
+
</>
|
|
438
|
+
),
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
settingsRoutes.post("/color-theme", async (c) => {
|
|
324
443
|
const i18n = getI18n(c);
|
|
325
|
-
const body = await c.req.json<{
|
|
326
|
-
const
|
|
444
|
+
const body = await c.req.json<{ theme: string }>();
|
|
445
|
+
const { settings } = c.var.services;
|
|
446
|
+
const themes = getAvailableThemes();
|
|
327
447
|
|
|
328
|
-
|
|
448
|
+
const validTheme = themes.find((t) => t.id === body.theme);
|
|
449
|
+
if (!validTheme) {
|
|
329
450
|
return dsToast(
|
|
330
451
|
i18n._(
|
|
331
452
|
msg({
|
|
332
|
-
message: "
|
|
333
|
-
comment: "@context: Error toast when
|
|
453
|
+
message: "That theme isn't available. Pick another one.",
|
|
454
|
+
comment: "@context: Error toast when selected theme is not valid",
|
|
334
455
|
}),
|
|
335
456
|
),
|
|
336
457
|
"error",
|
|
337
458
|
);
|
|
338
459
|
}
|
|
339
460
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
461
|
+
const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
|
|
462
|
+
if (validTheme.id === defaultThemeId) {
|
|
463
|
+
await settings.remove(SETTINGS_KEYS.THEME);
|
|
464
|
+
} else {
|
|
465
|
+
await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return dsRedirect("/settings/color-theme?saved");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// ===========================================================================
|
|
472
|
+
// Font Theme (moved from appearance routes)
|
|
473
|
+
// ===========================================================================
|
|
474
|
+
|
|
475
|
+
settingsRoutes.get("/font-theme", async (c) => {
|
|
476
|
+
const currentFontThemeId = c.var.allSettings["FONT_THEME"] ?? "default";
|
|
477
|
+
const saved = c.req.query("saved") !== undefined;
|
|
478
|
+
const navData = await getNavigationData(c);
|
|
479
|
+
|
|
480
|
+
return renderPublicPage(c, {
|
|
481
|
+
title: `Font Theme - ${navData.siteName}`,
|
|
482
|
+
navData,
|
|
483
|
+
toast: saved ? { message: "Font theme updated." } : undefined,
|
|
484
|
+
content: (
|
|
485
|
+
<>
|
|
486
|
+
<AdminBreadcrumb
|
|
487
|
+
parent="Settings"
|
|
488
|
+
parentHref="/settings"
|
|
489
|
+
current="Font Theme"
|
|
490
|
+
/>
|
|
491
|
+
<FontThemeContent
|
|
492
|
+
fontThemes={BUILTIN_FONT_THEMES}
|
|
493
|
+
currentFontThemeId={currentFontThemeId}
|
|
494
|
+
/>
|
|
495
|
+
</>
|
|
496
|
+
),
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
settingsRoutes.post("/font-theme", async (c) => {
|
|
501
|
+
const i18n = getI18n(c);
|
|
502
|
+
const body = await c.req.json<{ fontTheme: string }>();
|
|
503
|
+
const { settings } = c.var.services;
|
|
504
|
+
|
|
505
|
+
const validFont = BUILTIN_FONT_THEMES.find((f) => f.id === body.fontTheme);
|
|
506
|
+
if (!validFont) {
|
|
346
507
|
return dsToast(
|
|
347
508
|
i18n._(
|
|
348
509
|
msg({
|
|
349
|
-
message: "
|
|
350
|
-
comment:
|
|
510
|
+
message: "That font theme isn't available. Pick another one.",
|
|
511
|
+
comment:
|
|
512
|
+
"@context: Error toast when selected font theme is not valid",
|
|
351
513
|
}),
|
|
352
514
|
),
|
|
353
515
|
"error",
|
|
354
516
|
);
|
|
355
517
|
}
|
|
356
518
|
|
|
519
|
+
if (validFont.id === "default") {
|
|
520
|
+
await settings.remove("FONT_THEME");
|
|
521
|
+
} else {
|
|
522
|
+
await settings.set("FONT_THEME", validFont.id);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return dsRedirect("/settings/font-theme?saved");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// ===========================================================================
|
|
529
|
+
// Custom CSS (moved from appearance routes)
|
|
530
|
+
// ===========================================================================
|
|
531
|
+
|
|
532
|
+
settingsRoutes.get("/custom-css", async (c) => {
|
|
533
|
+
const customCSS = c.var.allSettings[SETTINGS_KEYS.CUSTOM_CSS] ?? "";
|
|
534
|
+
const navData = await getNavigationData(c);
|
|
535
|
+
|
|
536
|
+
return renderPublicPage(c, {
|
|
537
|
+
title: `Custom CSS - ${navData.siteName}`,
|
|
538
|
+
navData,
|
|
539
|
+
content: (
|
|
540
|
+
<>
|
|
541
|
+
<AdminBreadcrumb
|
|
542
|
+
parent="Settings"
|
|
543
|
+
parentHref="/settings"
|
|
544
|
+
current="Custom CSS"
|
|
545
|
+
/>
|
|
546
|
+
<AdvancedContent customCSS={customCSS} />
|
|
547
|
+
</>
|
|
548
|
+
),
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
settingsRoutes.post("/custom-css", async (c) => {
|
|
553
|
+
const i18n = getI18n(c);
|
|
554
|
+
const body = await c.req.json<{ customCSS: string }>();
|
|
555
|
+
const { settings } = c.var.services;
|
|
556
|
+
|
|
557
|
+
const css = body.customCSS?.trim() ?? "";
|
|
558
|
+
|
|
559
|
+
if (css) {
|
|
560
|
+
await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
|
|
561
|
+
} else {
|
|
562
|
+
await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
|
|
563
|
+
}
|
|
564
|
+
|
|
357
565
|
return dsToast(
|
|
358
566
|
i18n._(
|
|
359
567
|
msg({
|
|
360
|
-
message: "
|
|
361
|
-
comment: "@context: Toast after saving
|
|
568
|
+
message: "Custom CSS updated.",
|
|
569
|
+
comment: "@context: Toast after saving custom CSS",
|
|
362
570
|
}),
|
|
363
571
|
),
|
|
364
572
|
);
|
|
365
573
|
});
|
|
366
574
|
|
|
367
|
-
|
|
575
|
+
// ===========================================================================
|
|
576
|
+
// Account sub-menu
|
|
577
|
+
// ===========================================================================
|
|
578
|
+
|
|
579
|
+
settingsRoutes.get("/account", async (c) => {
|
|
580
|
+
const navData = await getNavigationData(c);
|
|
581
|
+
|
|
582
|
+
return renderPublicPage(c, {
|
|
583
|
+
title: `Account - ${navData.siteName}`,
|
|
584
|
+
navData,
|
|
585
|
+
content: (
|
|
586
|
+
<>
|
|
587
|
+
<AdminBreadcrumb
|
|
588
|
+
parent="Settings"
|
|
589
|
+
parentHref="/settings"
|
|
590
|
+
current="Account"
|
|
591
|
+
/>
|
|
592
|
+
<AccountMenuContent />
|
|
593
|
+
</>
|
|
594
|
+
),
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// ===========================================================================
|
|
599
|
+
// Sessions
|
|
600
|
+
// ===========================================================================
|
|
601
|
+
|
|
602
|
+
settingsRoutes.get("/account/sessions", async (c) => {
|
|
603
|
+
const navData = await getNavigationData(c);
|
|
604
|
+
|
|
605
|
+
// Get current session to mark it
|
|
606
|
+
const currentSession = await c.var.auth.api.getSession({
|
|
607
|
+
headers: c.req.raw.headers,
|
|
608
|
+
});
|
|
609
|
+
const currentToken = currentSession?.session?.token ?? "";
|
|
610
|
+
|
|
611
|
+
// List all active sessions
|
|
612
|
+
const rawSessions = await c.var.auth.api.listSessions({
|
|
613
|
+
headers: c.req.raw.headers,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const sessions: SessionInfo[] = (rawSessions ?? []).map(
|
|
617
|
+
(s: {
|
|
618
|
+
token: string;
|
|
619
|
+
ipAddress?: string | null;
|
|
620
|
+
userAgent?: string | null;
|
|
621
|
+
createdAt: Date;
|
|
622
|
+
}) => ({
|
|
623
|
+
token: s.token,
|
|
624
|
+
ipAddress: s.ipAddress ?? null,
|
|
625
|
+
userAgent: s.userAgent ?? null,
|
|
626
|
+
createdAt: Math.floor(new Date(s.createdAt).getTime() / 1000),
|
|
627
|
+
isCurrent: s.token === currentToken,
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
// Sort: current session first, then by creation date descending
|
|
632
|
+
sessions.sort((a, b) => {
|
|
633
|
+
if (a.isCurrent) return -1;
|
|
634
|
+
if (b.isCurrent) return 1;
|
|
635
|
+
return b.createdAt - a.createdAt;
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
return renderPublicPage(c, {
|
|
639
|
+
title: `Sessions - ${navData.siteName}`,
|
|
640
|
+
navData,
|
|
641
|
+
content: (
|
|
642
|
+
<>
|
|
643
|
+
<AdminBreadcrumb
|
|
644
|
+
parent="Account"
|
|
645
|
+
parentHref="/settings/account"
|
|
646
|
+
current="Sessions"
|
|
647
|
+
/>
|
|
648
|
+
<SessionsContent sessions={sessions} />
|
|
649
|
+
</>
|
|
650
|
+
),
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
settingsRoutes.post("/account/sessions/:token/revoke", async (c) => {
|
|
655
|
+
const token = c.req.param("token");
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
await c.var.auth.api.revokeSession({
|
|
659
|
+
body: { token },
|
|
660
|
+
headers: c.req.raw.headers,
|
|
661
|
+
});
|
|
662
|
+
} catch {
|
|
663
|
+
// Session may already be expired/revoked — still redirect
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return dsRedirect("/settings/account/sessions");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// ===========================================================================
|
|
670
|
+
// Password
|
|
671
|
+
// ===========================================================================
|
|
672
|
+
|
|
673
|
+
settingsRoutes.get("/account/password", async (c) => {
|
|
674
|
+
const navData = await getNavigationData(c);
|
|
675
|
+
|
|
676
|
+
return renderPublicPage(c, {
|
|
677
|
+
title: `Password - ${navData.siteName}`,
|
|
678
|
+
navData,
|
|
679
|
+
content: (
|
|
680
|
+
<>
|
|
681
|
+
<AdminBreadcrumb
|
|
682
|
+
parent="Account"
|
|
683
|
+
parentHref="/settings/account"
|
|
684
|
+
current="Password"
|
|
685
|
+
/>
|
|
686
|
+
<AccountContent />
|
|
687
|
+
</>
|
|
688
|
+
),
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
settingsRoutes.post("/account/password", async (c) => {
|
|
368
693
|
const i18n = getI18n(c);
|
|
369
694
|
const body = await c.req.json<{
|
|
370
695
|
currentPassword: string;
|
|
@@ -376,7 +701,8 @@ settingsRoutes.post("/password", async (c) => {
|
|
|
376
701
|
return dsToast(
|
|
377
702
|
i18n._(
|
|
378
703
|
msg({
|
|
379
|
-
message:
|
|
704
|
+
message:
|
|
705
|
+
"Passwords don't match. Make sure both fields are identical.",
|
|
380
706
|
comment:
|
|
381
707
|
"@context: Error toast when new password and confirmation differ",
|
|
382
708
|
}),
|
|
@@ -398,7 +724,7 @@ settingsRoutes.post("/password", async (c) => {
|
|
|
398
724
|
return dsToast(
|
|
399
725
|
i18n._(
|
|
400
726
|
msg({
|
|
401
|
-
message: "Current password
|
|
727
|
+
message: "Current password doesn't match. Try again.",
|
|
402
728
|
comment:
|
|
403
729
|
"@context: Error toast when current password verification fails",
|
|
404
730
|
}),
|
|
@@ -411,7 +737,7 @@ settingsRoutes.post("/password", async (c) => {
|
|
|
411
737
|
await stream.toast(
|
|
412
738
|
i18n._(
|
|
413
739
|
msg({
|
|
414
|
-
message: "Password changed
|
|
740
|
+
message: "Password changed.",
|
|
415
741
|
comment: "@context: Toast after changing account password",
|
|
416
742
|
}),
|
|
417
743
|
),
|
|
@@ -423,3 +749,53 @@ settingsRoutes.post("/password", async (c) => {
|
|
|
423
749
|
});
|
|
424
750
|
});
|
|
425
751
|
});
|
|
752
|
+
|
|
753
|
+
// ===========================================================================
|
|
754
|
+
// API Tokens
|
|
755
|
+
// ===========================================================================
|
|
756
|
+
|
|
757
|
+
settingsRoutes.get("/api-tokens", async (c) => {
|
|
758
|
+
const tokens = await c.var.services.apiTokens.list();
|
|
759
|
+
const navData = await getNavigationData(c);
|
|
760
|
+
const siteUrl = c.env.SITE_URL;
|
|
761
|
+
|
|
762
|
+
return renderPublicPage(c, {
|
|
763
|
+
title: `API Tokens - ${navData.siteName}`,
|
|
764
|
+
navData,
|
|
765
|
+
content: (
|
|
766
|
+
<>
|
|
767
|
+
<AdminBreadcrumb
|
|
768
|
+
parent="Settings"
|
|
769
|
+
parentHref="/settings"
|
|
770
|
+
current="API Tokens"
|
|
771
|
+
/>
|
|
772
|
+
<ApiTokensContent tokens={tokens} siteUrl={siteUrl} />
|
|
773
|
+
</>
|
|
774
|
+
),
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
settingsRoutes.post("/api-tokens", async (c) => {
|
|
779
|
+
const body = await c.req.json<{ tokenName: string }>();
|
|
780
|
+
const name = body.tokenName?.trim();
|
|
781
|
+
|
|
782
|
+
if (!name) {
|
|
783
|
+
return dsToast("Token name is required.", "error");
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const { plaintext } = await c.var.services.apiTokens.create(name);
|
|
787
|
+
|
|
788
|
+
return sse(c, async (stream) => {
|
|
789
|
+
await stream.patchSignals({
|
|
790
|
+
_newPlaintext: plaintext,
|
|
791
|
+
tokenName: "",
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
settingsRoutes.post("/api-tokens/:id/delete", async (c) => {
|
|
797
|
+
const id = c.req.param("id");
|
|
798
|
+
await c.var.services.apiTokens.delete(id);
|
|
799
|
+
|
|
800
|
+
return dsRedirect("/settings/api-tokens");
|
|
801
|
+
});
|