@jant/core 0.3.24 → 0.3.26
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/app.js +101 -571
- package/dist/client.js +1 -0
- package/dist/db/schema.js +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +3 -9
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -9
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +48 -3
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +16 -11
- package/dist/lib/schemas.js +34 -3
- package/dist/lib/theme.js +4 -4
- package/dist/lib/timeline.js +24 -48
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +3 -3
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +3 -3
- package/dist/routes/api/search.js +2 -2
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -416
- package/dist/routes/dash/index.js +1 -1
- package/dist/routes/dash/media.js +13 -393
- package/dist/routes/dash/pages.js +112 -86
- package/dist/routes/dash/posts.js +3 -5
- package/dist/routes/dash/redirects.js +20 -14
- package/dist/routes/dash/settings.js +213 -518
- package/dist/routes/feed/rss.js +4 -3
- package/dist/routes/feed/sitemap.js +5 -3
- package/dist/routes/pages/archive.js +3 -6
- package/dist/routes/pages/collection.js +3 -6
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +36 -0
- package/dist/routes/pages/home.js +33 -49
- package/dist/routes/pages/latest.js +45 -0
- package/dist/routes/pages/page.js +29 -32
- package/dist/routes/pages/post.js +3 -6
- package/dist/routes/pages/search.js +3 -6
- package/dist/services/page.js +5 -1
- package/dist/services/post.js +45 -31
- package/dist/services/search.js +1 -1
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/{theme → ui}/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +467 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
- package/dist/{theme/components → ui/dash}/PostList.js +6 -6
- package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
- package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
- package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
- package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +169 -0
- package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
- package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
- package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
- package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
- package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
- package/dist/ui/shared/index.js +5 -0
- package/package.json +1 -9
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/app.tsx +131 -561
- package/src/client.ts +1 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +1 -1
- package/src/i18n/locales/en.po +477 -261
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +477 -261
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +477 -261
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +7 -36
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/schemas.test.ts +60 -19
- package/src/lib/__tests__/timeline.test.ts +45 -81
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +15 -9
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -10
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +73 -4
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +22 -15
- package/src/lib/schemas.ts +47 -6
- package/src/lib/theme.ts +5 -5
- package/src/lib/timeline.ts +28 -57
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +3 -3
- package/src/preset.css +2 -1
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +3 -3
- package/src/routes/api/search.ts +2 -2
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +18 -367
- package/src/routes/dash/index.tsx +1 -1
- package/src/routes/dash/media.tsx +13 -415
- package/src/routes/dash/pages.tsx +131 -98
- package/src/routes/dash/posts.tsx +3 -7
- package/src/routes/dash/redirects.tsx +22 -16
- package/src/routes/dash/settings.tsx +265 -478
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +5 -3
- package/src/routes/feed/sitemap.ts +5 -3
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +2 -6
- package/src/routes/pages/collection.tsx +2 -6
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +44 -0
- package/src/routes/pages/home.tsx +30 -53
- package/src/routes/pages/latest.tsx +59 -0
- package/src/routes/pages/page.tsx +28 -30
- package/src/routes/pages/post.tsx +2 -5
- package/src/routes/pages/search.tsx +2 -6
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post.test.ts +114 -15
- package/src/services/page.ts +13 -1
- package/src/services/post.ts +58 -40
- package/src/services/search.ts +2 -2
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +475 -0
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -774
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/{theme → ui}/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +414 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
- package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
- package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
- package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
- package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
- package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +164 -0
- package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
- package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
- package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
- package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
- package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
- package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -46
- package/dist/routes/dash/navigation.js +0 -289
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
- package/dist/themes/threads/index.js +0 -81
- package/dist/themes/threads/pages/HomePage.js +0 -25
- package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
- package/dist/themes/threads/timeline/TimelineItem.js +0 -36
- package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
- package/dist/themes/threads/timeline/groupByDate.js +0 -22
- package/dist/themes/threads/timeline/timelineMore.js +0 -107
- package/src/lib/__tests__/theme-components.test.ts +0 -105
- package/src/lib/theme-components.ts +0 -65
- package/src/routes/dash/navigation.tsx +0 -317
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
- package/src/themes/threads/index.ts +0 -100
- package/src/themes/threads/style.css +0 -336
- package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
- package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
- package/src/themes/threads/timeline/groupByDate.ts +0 -30
- package/src/themes/threads/timeline/timelineMore.tsx +0 -130
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Reset Routes
|
|
3
|
+
*
|
|
4
|
+
* One-time token-based password reset flow.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import { hashPassword } from "better-auth/crypto";
|
|
11
|
+
import type { Bindings } from "../../types.js";
|
|
12
|
+
import type { AppVariables } from "../../app.js";
|
|
13
|
+
import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
|
|
14
|
+
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
15
|
+
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
16
|
+
import { ResetPasswordSchema } from "../../lib/schemas.js";
|
|
17
|
+
|
|
18
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
19
|
+
|
|
20
|
+
const ResetContent: FC<{ token: string }> = ({ token }) => {
|
|
21
|
+
const { t } = useLingui();
|
|
22
|
+
const signals = JSON.stringify({
|
|
23
|
+
password: "",
|
|
24
|
+
confirmPassword: "",
|
|
25
|
+
token,
|
|
26
|
+
}).replace(/</g, "\\u003c");
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
30
|
+
<div class="card max-w-md w-full">
|
|
31
|
+
<header>
|
|
32
|
+
<h2>
|
|
33
|
+
{t({
|
|
34
|
+
message: "Reset Password",
|
|
35
|
+
comment: "@context: Password reset page heading",
|
|
36
|
+
})}
|
|
37
|
+
</h2>
|
|
38
|
+
<p>
|
|
39
|
+
{t({
|
|
40
|
+
message: "Enter your new password.",
|
|
41
|
+
comment: "@context: Password reset page description",
|
|
42
|
+
})}
|
|
43
|
+
</p>
|
|
44
|
+
</header>
|
|
45
|
+
<section>
|
|
46
|
+
<form
|
|
47
|
+
data-signals={signals}
|
|
48
|
+
data-on:submit__prevent="@post('/reset')"
|
|
49
|
+
data-indicator="_loading"
|
|
50
|
+
class="flex flex-col gap-4"
|
|
51
|
+
>
|
|
52
|
+
<div class="field">
|
|
53
|
+
<label class="label">
|
|
54
|
+
{t({
|
|
55
|
+
message: "New Password",
|
|
56
|
+
comment: "@context: Password reset form field",
|
|
57
|
+
})}
|
|
58
|
+
</label>
|
|
59
|
+
<input
|
|
60
|
+
type="password"
|
|
61
|
+
data-bind="password"
|
|
62
|
+
class="input"
|
|
63
|
+
required
|
|
64
|
+
minLength={8}
|
|
65
|
+
autocomplete="new-password"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="field">
|
|
69
|
+
<label class="label">
|
|
70
|
+
{t({
|
|
71
|
+
message: "Confirm Password",
|
|
72
|
+
comment: "@context: Password reset form field",
|
|
73
|
+
})}
|
|
74
|
+
</label>
|
|
75
|
+
<input
|
|
76
|
+
type="password"
|
|
77
|
+
data-bind="confirmPassword"
|
|
78
|
+
class="input"
|
|
79
|
+
required
|
|
80
|
+
minLength={8}
|
|
81
|
+
autocomplete="new-password"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
85
|
+
<svg
|
|
86
|
+
data-show="$_loading"
|
|
87
|
+
style="display:none"
|
|
88
|
+
class="animate-spin size-4"
|
|
89
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke="currentColor"
|
|
93
|
+
stroke-width="2"
|
|
94
|
+
stroke-linecap="round"
|
|
95
|
+
stroke-linejoin="round"
|
|
96
|
+
role="status"
|
|
97
|
+
>
|
|
98
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
99
|
+
</svg>
|
|
100
|
+
{t({
|
|
101
|
+
message: "Reset Password",
|
|
102
|
+
comment: "@context: Password reset form submit button",
|
|
103
|
+
})}
|
|
104
|
+
</button>
|
|
105
|
+
</form>
|
|
106
|
+
</section>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const ResetErrorContent: FC = () => {
|
|
113
|
+
const { t } = useLingui();
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
117
|
+
<div class="card max-w-md w-full">
|
|
118
|
+
<header>
|
|
119
|
+
<h2>
|
|
120
|
+
{t({
|
|
121
|
+
message: "Invalid or Expired Link",
|
|
122
|
+
comment: "@context: Password reset error heading",
|
|
123
|
+
})}
|
|
124
|
+
</h2>
|
|
125
|
+
</header>
|
|
126
|
+
<section>
|
|
127
|
+
<p class="text-muted-foreground">
|
|
128
|
+
{t({
|
|
129
|
+
message:
|
|
130
|
+
"This password reset link is invalid or has expired. Please generate a new one.",
|
|
131
|
+
comment: "@context: Password reset error description",
|
|
132
|
+
})}
|
|
133
|
+
</p>
|
|
134
|
+
</section>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validate a password reset token against the stored value.
|
|
142
|
+
* Returns true if the token is valid and not expired.
|
|
143
|
+
*/
|
|
144
|
+
async function validateResetToken(
|
|
145
|
+
settings: { get(key: string): Promise<string | null> },
|
|
146
|
+
token: string,
|
|
147
|
+
): Promise<boolean> {
|
|
148
|
+
const stored = await settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
149
|
+
if (!stored) return false;
|
|
150
|
+
|
|
151
|
+
const separatorIndex = stored.lastIndexOf(":");
|
|
152
|
+
const storedToken = stored.substring(0, separatorIndex);
|
|
153
|
+
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
154
|
+
const now = Math.floor(Date.now() / 1000);
|
|
155
|
+
|
|
156
|
+
return token === storedToken && now <= expiry;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const resetRoutes = new Hono<Env>();
|
|
160
|
+
|
|
161
|
+
resetRoutes.get("/reset", async (c) => {
|
|
162
|
+
const token = c.req.query("token");
|
|
163
|
+
if (!token) {
|
|
164
|
+
return c.html(
|
|
165
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
166
|
+
<ResetErrorContent />
|
|
167
|
+
</BaseLayout>,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const isValid = await validateResetToken(c.var.services.settings, token);
|
|
172
|
+
if (!isValid) {
|
|
173
|
+
return c.html(
|
|
174
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
175
|
+
<ResetErrorContent />
|
|
176
|
+
</BaseLayout>,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return c.html(
|
|
181
|
+
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
182
|
+
<ResetContent token={token} />
|
|
183
|
+
</BaseLayout>,
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
resetRoutes.post("/reset", async (c) => {
|
|
188
|
+
const body = await c.req.json();
|
|
189
|
+
const parsed = ResetPasswordSchema.safeParse(body);
|
|
190
|
+
|
|
191
|
+
if (!parsed.success) {
|
|
192
|
+
const msg = parsed.error.errors[0]?.message ?? "Invalid input";
|
|
193
|
+
return dsToast(msg, "error");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { password, token } = parsed.data;
|
|
197
|
+
|
|
198
|
+
// Validate token
|
|
199
|
+
const isValid = await validateResetToken(c.var.services.settings, token);
|
|
200
|
+
if (!isValid) {
|
|
201
|
+
return dsToast("Invalid or expired reset link.", "error");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const hashedPassword = await hashPassword(password);
|
|
206
|
+
const db = c.env.DB.withSession() as unknown as D1Database;
|
|
207
|
+
|
|
208
|
+
// Get admin user
|
|
209
|
+
const userResult = await db
|
|
210
|
+
.prepare("SELECT id FROM user LIMIT 1")
|
|
211
|
+
.first<{ id: string }>();
|
|
212
|
+
if (!userResult) {
|
|
213
|
+
return dsToast("No user account found.", "error");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Update password
|
|
217
|
+
await db
|
|
218
|
+
.prepare(
|
|
219
|
+
"UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'",
|
|
220
|
+
)
|
|
221
|
+
.bind(hashedPassword, userResult.id)
|
|
222
|
+
.run();
|
|
223
|
+
|
|
224
|
+
// Delete all sessions
|
|
225
|
+
await db
|
|
226
|
+
.prepare("DELETE FROM session WHERE user_id = ?")
|
|
227
|
+
.bind(userResult.id)
|
|
228
|
+
.run();
|
|
229
|
+
|
|
230
|
+
// Delete the reset token
|
|
231
|
+
await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
232
|
+
|
|
233
|
+
return dsRedirect("/signin?reset");
|
|
234
|
+
} catch (err) {
|
|
235
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
236
|
+
console.error("Password reset error:", err);
|
|
237
|
+
return dsToast("Failed to reset password.", "error");
|
|
238
|
+
}
|
|
239
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup Routes
|
|
3
|
+
*
|
|
4
|
+
* Initial admin account creation during first-time setup.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { Bindings } from "../../types.js";
|
|
11
|
+
import type { AppVariables } from "../../app.js";
|
|
12
|
+
import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
|
|
13
|
+
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
14
|
+
import { SetupSchema } from "../../lib/schemas.js";
|
|
15
|
+
import { mapIanaToTimezone } from "../../lib/timezones.js";
|
|
16
|
+
|
|
17
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
18
|
+
|
|
19
|
+
const SetupContent: FC = () => {
|
|
20
|
+
const { t } = useLingui();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
24
|
+
<div class="card max-w-md w-full">
|
|
25
|
+
<header>
|
|
26
|
+
<h2>
|
|
27
|
+
{t({
|
|
28
|
+
message: "Welcome to Jant",
|
|
29
|
+
comment: "@context: Setup page welcome heading",
|
|
30
|
+
})}
|
|
31
|
+
</h2>
|
|
32
|
+
<p>
|
|
33
|
+
{t({
|
|
34
|
+
message: "Create your admin account.",
|
|
35
|
+
comment: "@context: Setup page description",
|
|
36
|
+
})}
|
|
37
|
+
</p>
|
|
38
|
+
</header>
|
|
39
|
+
<section>
|
|
40
|
+
<form
|
|
41
|
+
data-signals="{name: '', email: '', password: '', _timezone: ''}"
|
|
42
|
+
data-init="$_timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''"
|
|
43
|
+
data-on:submit__prevent="@post('/setup')"
|
|
44
|
+
data-indicator="_loading"
|
|
45
|
+
class="flex flex-col gap-4"
|
|
46
|
+
>
|
|
47
|
+
<div class="field">
|
|
48
|
+
<label class="label">
|
|
49
|
+
{t({
|
|
50
|
+
message: "Your Name",
|
|
51
|
+
comment: "@context: Setup form field - user name",
|
|
52
|
+
})}
|
|
53
|
+
</label>
|
|
54
|
+
<input
|
|
55
|
+
type="text"
|
|
56
|
+
data-bind="name"
|
|
57
|
+
class="input"
|
|
58
|
+
required
|
|
59
|
+
placeholder="John Doe"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="field">
|
|
63
|
+
<label class="label">
|
|
64
|
+
{t({
|
|
65
|
+
message: "Email",
|
|
66
|
+
comment: "@context: Setup/signin form field - email",
|
|
67
|
+
})}
|
|
68
|
+
</label>
|
|
69
|
+
<input
|
|
70
|
+
type="email"
|
|
71
|
+
data-bind="email"
|
|
72
|
+
class="input"
|
|
73
|
+
required
|
|
74
|
+
placeholder="you@example.com"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="field">
|
|
78
|
+
<label class="label">
|
|
79
|
+
{t({
|
|
80
|
+
message: "Password",
|
|
81
|
+
comment: "@context: Setup/signin form field - password",
|
|
82
|
+
})}
|
|
83
|
+
</label>
|
|
84
|
+
<input
|
|
85
|
+
type="password"
|
|
86
|
+
data-bind="password"
|
|
87
|
+
class="input"
|
|
88
|
+
required
|
|
89
|
+
minLength={8}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
93
|
+
<svg
|
|
94
|
+
data-show="$_loading"
|
|
95
|
+
style="display:none"
|
|
96
|
+
class="animate-spin size-4"
|
|
97
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
98
|
+
viewBox="0 0 24 24"
|
|
99
|
+
fill="none"
|
|
100
|
+
stroke="currentColor"
|
|
101
|
+
stroke-width="2"
|
|
102
|
+
stroke-linecap="round"
|
|
103
|
+
stroke-linejoin="round"
|
|
104
|
+
role="status"
|
|
105
|
+
>
|
|
106
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
107
|
+
</svg>
|
|
108
|
+
{t({
|
|
109
|
+
message: "Complete Setup",
|
|
110
|
+
comment: "@context: Setup form submit button",
|
|
111
|
+
})}
|
|
112
|
+
</button>
|
|
113
|
+
</form>
|
|
114
|
+
</section>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const setupRoutes = new Hono<Env>();
|
|
121
|
+
|
|
122
|
+
setupRoutes.get("/setup", async (c) => {
|
|
123
|
+
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
124
|
+
if (isComplete) return c.redirect("/");
|
|
125
|
+
|
|
126
|
+
return c.html(
|
|
127
|
+
<BaseLayout title="Setup - Jant" c={c}>
|
|
128
|
+
<SetupContent />
|
|
129
|
+
</BaseLayout>,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
setupRoutes.post("/setup", async (c) => {
|
|
134
|
+
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
135
|
+
if (isComplete) return c.redirect("/");
|
|
136
|
+
|
|
137
|
+
const body = await c.req.json<Record<string, string>>();
|
|
138
|
+
const parsed = SetupSchema.safeParse(body);
|
|
139
|
+
const browserTimezone = body._timezone;
|
|
140
|
+
|
|
141
|
+
if (!parsed.success) {
|
|
142
|
+
const msg = parsed.error.errors[0]?.message ?? "Invalid input";
|
|
143
|
+
return dsToast(msg, "error");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { name, email, password } = parsed.data;
|
|
147
|
+
|
|
148
|
+
if (!c.var.auth) {
|
|
149
|
+
return dsToast("AUTH_SECRET not configured", "error");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const signUpResponse = await c.var.auth.api.signUpEmail({
|
|
154
|
+
body: { name, email, password },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!signUpResponse || "error" in signUpResponse) {
|
|
158
|
+
return dsToast("Failed to create account", "error");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await c.var.services.settings.completeOnboarding();
|
|
162
|
+
|
|
163
|
+
// Save auto-detected timezone
|
|
164
|
+
if (browserTimezone) {
|
|
165
|
+
const tz = mapIanaToTimezone(browserTimezone);
|
|
166
|
+
if (tz !== "UTC") {
|
|
167
|
+
await c.var.services.settings.set("TIME_ZONE", tz);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Seed default navigation items
|
|
172
|
+
await c.var.services.navItems.create({
|
|
173
|
+
type: "link",
|
|
174
|
+
label: "Featured",
|
|
175
|
+
url: "/featured",
|
|
176
|
+
});
|
|
177
|
+
await c.var.services.navItems.create({
|
|
178
|
+
type: "link",
|
|
179
|
+
label: "Collections",
|
|
180
|
+
url: "/collections",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return dsRedirect("/signin?setup");
|
|
184
|
+
} catch (err) {
|
|
185
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
186
|
+
console.error("Setup error:", err);
|
|
187
|
+
return dsToast("Failed to create account", "error");
|
|
188
|
+
}
|
|
189
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-in / Sign-out Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import type { FC } from "hono/jsx";
|
|
7
|
+
import { useLingui } from "@lingui/react/macro";
|
|
8
|
+
import type { Bindings } from "../../types.js";
|
|
9
|
+
import type { AppVariables } from "../../app.js";
|
|
10
|
+
import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
|
|
11
|
+
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
12
|
+
import { SigninSchema } from "../../lib/schemas.js";
|
|
13
|
+
|
|
14
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
|
+
|
|
16
|
+
const SigninContent: FC<{
|
|
17
|
+
demoEmail?: string;
|
|
18
|
+
demoPassword?: string;
|
|
19
|
+
}> = ({ demoEmail, demoPassword }) => {
|
|
20
|
+
const { t } = useLingui();
|
|
21
|
+
const signals = JSON.stringify({
|
|
22
|
+
email: demoEmail || "",
|
|
23
|
+
password: demoPassword || "",
|
|
24
|
+
}).replace(/</g, "\\u003c");
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div class="min-h-screen flex items-center justify-center">
|
|
28
|
+
<div class="card max-w-md w-full">
|
|
29
|
+
<header>
|
|
30
|
+
<h2>
|
|
31
|
+
{t({
|
|
32
|
+
message: "Sign In",
|
|
33
|
+
comment: "@context: Sign in page heading",
|
|
34
|
+
})}
|
|
35
|
+
</h2>
|
|
36
|
+
</header>
|
|
37
|
+
<section>
|
|
38
|
+
{demoEmail && demoPassword && (
|
|
39
|
+
<p class="text-muted-foreground text-sm mb-4">
|
|
40
|
+
{t({
|
|
41
|
+
message: "Demo account pre-filled. Just click Sign In.",
|
|
42
|
+
comment:
|
|
43
|
+
"@context: Hint shown on signin page when demo credentials are pre-filled",
|
|
44
|
+
})}
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
<form
|
|
48
|
+
data-signals={signals}
|
|
49
|
+
data-on:submit__prevent="@post('/signin')"
|
|
50
|
+
data-indicator="_loading"
|
|
51
|
+
class="flex flex-col gap-4"
|
|
52
|
+
>
|
|
53
|
+
<div class="field">
|
|
54
|
+
<label class="label">
|
|
55
|
+
{t({
|
|
56
|
+
message: "Email",
|
|
57
|
+
comment: "@context: Setup/signin form field - email",
|
|
58
|
+
})}
|
|
59
|
+
</label>
|
|
60
|
+
<input type="email" data-bind="email" class="input" required />
|
|
61
|
+
</div>
|
|
62
|
+
<div class="field">
|
|
63
|
+
<label class="label">
|
|
64
|
+
{t({
|
|
65
|
+
message: "Password",
|
|
66
|
+
comment: "@context: Setup/signin form field - password",
|
|
67
|
+
})}
|
|
68
|
+
</label>
|
|
69
|
+
<input
|
|
70
|
+
type="password"
|
|
71
|
+
data-bind="password"
|
|
72
|
+
class="input"
|
|
73
|
+
required
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
77
|
+
<svg
|
|
78
|
+
data-show="$_loading"
|
|
79
|
+
style="display:none"
|
|
80
|
+
class="animate-spin size-4"
|
|
81
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
fill="none"
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
stroke-width="2"
|
|
86
|
+
stroke-linecap="round"
|
|
87
|
+
stroke-linejoin="round"
|
|
88
|
+
role="status"
|
|
89
|
+
>
|
|
90
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
91
|
+
</svg>
|
|
92
|
+
{t({
|
|
93
|
+
message: "Sign In",
|
|
94
|
+
comment: "@context: Sign in form submit button",
|
|
95
|
+
})}
|
|
96
|
+
</button>
|
|
97
|
+
</form>
|
|
98
|
+
</section>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const signinRoutes = new Hono<Env>();
|
|
105
|
+
|
|
106
|
+
signinRoutes.get("/signin", async (c) => {
|
|
107
|
+
const isSetup = c.req.query("setup") !== undefined;
|
|
108
|
+
const isReset = c.req.query("reset") !== undefined;
|
|
109
|
+
let toast: { message: string } | undefined;
|
|
110
|
+
if (isSetup) {
|
|
111
|
+
toast = { message: "Account created successfully. Please sign in." };
|
|
112
|
+
} else if (isReset) {
|
|
113
|
+
toast = { message: "Password reset successfully. Please sign in." };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return c.html(
|
|
117
|
+
<BaseLayout title="Sign In - Jant" c={c} toast={toast}>
|
|
118
|
+
<SigninContent
|
|
119
|
+
demoEmail={c.env.DEMO_EMAIL}
|
|
120
|
+
demoPassword={c.env.DEMO_PASSWORD}
|
|
121
|
+
/>
|
|
122
|
+
</BaseLayout>,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
signinRoutes.post("/signin", async (c) => {
|
|
127
|
+
if (!c.var.auth) {
|
|
128
|
+
return dsToast("Auth not configured", "error");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const body = await c.req.json();
|
|
132
|
+
const parsed = SigninSchema.safeParse(body);
|
|
133
|
+
|
|
134
|
+
if (!parsed.success) {
|
|
135
|
+
const msg = parsed.error.errors[0]?.message ?? "Invalid input";
|
|
136
|
+
return dsToast(msg, "error");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { email, password } = parsed.data;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const { headers } = await c.var.auth.api.signInEmail({
|
|
143
|
+
returnHeaders: true,
|
|
144
|
+
body: { email, password },
|
|
145
|
+
headers: c.req.raw.headers,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return dsRedirect("/dash", { headers });
|
|
149
|
+
} catch {
|
|
150
|
+
return dsToast("Invalid email or password", "error");
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
signinRoutes.get("/signout", async (c) => {
|
|
155
|
+
if (c.var.auth) {
|
|
156
|
+
try {
|
|
157
|
+
await c.var.auth.api.signOut({ headers: c.req.raw.headers });
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore signout errors
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return c.redirect("/");
|
|
163
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose Route
|
|
3
|
+
*
|
|
4
|
+
* Handles post creation from the public-site compose dialog.
|
|
5
|
+
* Returns dsRedirect to the new post's permalink (Datastar form pattern).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import type { Bindings } from "../types.js";
|
|
10
|
+
import type { AppVariables } from "../app.js";
|
|
11
|
+
import { requireAuth } from "../middleware/auth.js";
|
|
12
|
+
import { CreatePostSchema, validateMediaCount } from "../lib/schemas.js";
|
|
13
|
+
import * as sqid from "../lib/sqid.js";
|
|
14
|
+
import { dsRedirect, dsToast } from "../lib/sse.js";
|
|
15
|
+
|
|
16
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
|
+
|
|
18
|
+
export const composeRoutes = new Hono<Env>();
|
|
19
|
+
|
|
20
|
+
// All compose routes require authentication
|
|
21
|
+
composeRoutes.use("*", requireAuth());
|
|
22
|
+
|
|
23
|
+
composeRoutes.post("/", async (c) => {
|
|
24
|
+
const raw = await c.req.json();
|
|
25
|
+
|
|
26
|
+
const result = CreatePostSchema.safeParse(raw);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
const firstError = result.error.issues[0]?.message ?? "Invalid input";
|
|
29
|
+
return dsToast(firstError, "error");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = result.data;
|
|
33
|
+
|
|
34
|
+
// Validate media count
|
|
35
|
+
if (data.mediaIds) {
|
|
36
|
+
const mediaError = validateMediaCount(data.mediaIds);
|
|
37
|
+
if (mediaError) {
|
|
38
|
+
return dsToast(mediaError, "error");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const post = await c.var.services.posts.create({
|
|
43
|
+
format: data.format,
|
|
44
|
+
title: data.title || undefined,
|
|
45
|
+
body: data.body || undefined,
|
|
46
|
+
status: data.status ?? "published",
|
|
47
|
+
featured: data.featured,
|
|
48
|
+
pinned: data.pinned,
|
|
49
|
+
url: data.url || undefined,
|
|
50
|
+
quoteText: data.quoteText || undefined,
|
|
51
|
+
rating: data.rating || undefined,
|
|
52
|
+
collectionId: data.collectionId || undefined,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Attach media if provided
|
|
56
|
+
if (data.mediaIds && data.mediaIds.length > 0) {
|
|
57
|
+
await c.var.services.media.attachToPost(post.id, data.mediaIds);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Redirect to the new post's permalink
|
|
61
|
+
const permalink = post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`;
|
|
62
|
+
return dsRedirect(permalink);
|
|
63
|
+
});
|