@meith/web 0.33.0 → 0.33.1
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/app/admin/system/page.tsx +15 -1
- package/app/redirect/page.tsx +5 -4
- package/package.json +49 -49
- package/src/components/admin/theme-forms.tsx +1 -1
- package/src/server/image-upload.ts +31 -1
- package/src/server/system-admin.ts +13 -9
- package/src/server/upgrade-notice.ts +1 -1
- package/src/view/redirect-notice.ts +19 -2
|
@@ -53,7 +53,7 @@ export default async function AdminSystemPage() {
|
|
|
53
53
|
)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const { mail, scheduler, volumes } = view
|
|
56
|
+
const { mail, scheduler, volumes, legacyPasswordHashes } = view
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
59
|
<PanelPage
|
|
@@ -252,6 +252,20 @@ export default async function AdminSystemPage() {
|
|
|
252
252
|
</ul>
|
|
253
253
|
</section>
|
|
254
254
|
|
|
255
|
+
{legacyPasswordHashes > 0 && (
|
|
256
|
+
<section className={PANEL_CARD}>
|
|
257
|
+
<h2 className="font-heading text-lg font-semibold">
|
|
258
|
+
{translator.t('adminSystem.legacyPasswords')}
|
|
259
|
+
</h2>
|
|
260
|
+
<p className="text-sm">
|
|
261
|
+
{translator.t('adminSystem.legacyPasswordsCount', { count: legacyPasswordHashes })}
|
|
262
|
+
</p>
|
|
263
|
+
<p className="text-sm text-muted-foreground">
|
|
264
|
+
{translator.t('adminSystem.legacyPasswordsHint')}
|
|
265
|
+
</p>
|
|
266
|
+
</section>
|
|
267
|
+
)}
|
|
268
|
+
|
|
255
269
|
<section className={PANEL_CARD}>
|
|
256
270
|
<h2 className="font-heading text-lg font-semibold">
|
|
257
271
|
{translator.t('adminSystem.migrations')}
|
package/app/redirect/page.tsx
CHANGED
|
@@ -15,12 +15,13 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
15
15
|
export default async function RedirectPage({
|
|
16
16
|
searchParams,
|
|
17
17
|
}: {
|
|
18
|
-
searchParams: Promise<{ to?: string;
|
|
18
|
+
searchParams: Promise<{ to?: string; m?: string }>
|
|
19
19
|
}) {
|
|
20
|
-
const { to,
|
|
20
|
+
const { to, m } = await searchParams
|
|
21
|
+
const translator = await getTranslator()
|
|
21
22
|
const filtered = await filterView(
|
|
22
23
|
'view.redirect-notice',
|
|
23
|
-
buildRedirectNotice(to,
|
|
24
|
+
buildRedirectNotice(to, m, translator),
|
|
24
25
|
viewerRef(await getActor()),
|
|
25
26
|
)
|
|
26
27
|
const notice = { ...filtered, targetHref: localHref(filtered.targetHref) }
|
|
@@ -36,7 +37,7 @@ export default async function RedirectPage({
|
|
|
36
37
|
>
|
|
37
38
|
<RedirectNotice
|
|
38
39
|
{...notice}
|
|
39
|
-
copy={slotCopy(await currentTheme(), 'RedirectNotice',
|
|
40
|
+
copy={slotCopy(await currentTheme(), 'RedirectNotice', translator)}
|
|
40
41
|
/>
|
|
41
42
|
</main>
|
|
42
43
|
</>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.1",
|
|
4
4
|
"description": "The board itself: the Next.js app, and the forum-web bin that materializes it into an external board workspace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,54 +43,54 @@
|
|
|
43
43
|
"react-dom": "19.2.8",
|
|
44
44
|
"tailwindcss": "^4.3.3",
|
|
45
45
|
"typescript": "7.0.2",
|
|
46
|
-
"@meith/accounts": "0.33.
|
|
47
|
-
"@meith/admin": "0.33.
|
|
48
|
-
"@meith/antispam": "0.33.
|
|
49
|
-
"@meith/api": "0.33.
|
|
50
|
-
"@meith/attachments": "0.33.
|
|
51
|
-
"@meith/authorization": "0.33.
|
|
52
|
-
"@meith/avatars": "0.33.
|
|
53
|
-
"@meith/board-digest": "0.33.
|
|
54
|
-
"@meith/core": "0.33.
|
|
55
|
-
"@meith/db": "0.33.
|
|
56
|
-
"@meith/demo": "0.33.
|
|
57
|
-
"@meith/drafts": "0.33.
|
|
58
|
-
"@meith/drivers": "0.33.
|
|
59
|
-
"@meith/events": "0.33.
|
|
60
|
-
"@meith/forums": "0.33.
|
|
61
|
-
"@meith/groups": "0.33.
|
|
62
|
-
"@meith/i18n": "0.33.
|
|
63
|
-
"@meith/import": "0.33.
|
|
64
|
-
"@meith/install": "0.33.
|
|
65
|
-
"@meith/mail": "0.33.
|
|
66
|
-
"@meith/markdown": "0.33.
|
|
67
|
-
"@meith/marketplace": "0.33.
|
|
68
|
-
"@meith/messages": "0.33.
|
|
69
|
-
"@meith/moderation": "0.33.
|
|
70
|
-
"@meith/notifications": "0.33.
|
|
71
|
-
"@meith/plugin-calendar": "0.33.
|
|
72
|
-
"@meith/plugin-dues": "0.33.
|
|
73
|
-
"@meith/plugin-kit": "0.33.
|
|
74
|
-
"@meith/polls": "0.33.
|
|
75
|
-
"@meith/posts": "0.33.
|
|
76
|
-
"@meith/profile-fields": "0.33.
|
|
77
|
-
"@meith/relations": "0.33.
|
|
78
|
-
"@meith/reputation": "0.33.
|
|
79
|
-
"@meith/runtime": "0.33.
|
|
80
|
-
"@meith/search": "0.33.
|
|
81
|
-
"@meith/settings": "0.33.
|
|
82
|
-
"@meith/signatures": "0.33.
|
|
83
|
-
"@meith/subscriptions": "0.33.
|
|
84
|
-
"@meith/tasks": "0.33.
|
|
85
|
-
"@meith/theme-clubhouse": "0.33.
|
|
86
|
-
"@meith/theme-default": "0.33.
|
|
87
|
-
"@meith/theme-kit": "0.33.
|
|
88
|
-
"@meith/theme-midnight": "0.33.
|
|
89
|
-
"@meith/theme-phasebook": "0.33.
|
|
90
|
-
"@meith/theme-raidframe": "0.33.
|
|
91
|
-
"@meith/threads": "0.33.
|
|
92
|
-
"@meith/ui": "0.33.
|
|
93
|
-
"@meith/upgrade": "0.33.
|
|
46
|
+
"@meith/accounts": "0.33.1",
|
|
47
|
+
"@meith/admin": "0.33.1",
|
|
48
|
+
"@meith/antispam": "0.33.1",
|
|
49
|
+
"@meith/api": "0.33.1",
|
|
50
|
+
"@meith/attachments": "0.33.1",
|
|
51
|
+
"@meith/authorization": "0.33.1",
|
|
52
|
+
"@meith/avatars": "0.33.1",
|
|
53
|
+
"@meith/board-digest": "0.33.1",
|
|
54
|
+
"@meith/core": "0.33.1",
|
|
55
|
+
"@meith/db": "0.33.1",
|
|
56
|
+
"@meith/demo": "0.33.1",
|
|
57
|
+
"@meith/drafts": "0.33.1",
|
|
58
|
+
"@meith/drivers": "0.33.1",
|
|
59
|
+
"@meith/events": "0.33.1",
|
|
60
|
+
"@meith/forums": "0.33.1",
|
|
61
|
+
"@meith/groups": "0.33.1",
|
|
62
|
+
"@meith/i18n": "0.33.1",
|
|
63
|
+
"@meith/import": "0.33.1",
|
|
64
|
+
"@meith/install": "0.33.1",
|
|
65
|
+
"@meith/mail": "0.33.1",
|
|
66
|
+
"@meith/markdown": "0.33.1",
|
|
67
|
+
"@meith/marketplace": "0.33.1",
|
|
68
|
+
"@meith/messages": "0.33.1",
|
|
69
|
+
"@meith/moderation": "0.33.1",
|
|
70
|
+
"@meith/notifications": "0.33.1",
|
|
71
|
+
"@meith/plugin-calendar": "0.33.1",
|
|
72
|
+
"@meith/plugin-dues": "0.33.1",
|
|
73
|
+
"@meith/plugin-kit": "0.33.1",
|
|
74
|
+
"@meith/polls": "0.33.1",
|
|
75
|
+
"@meith/posts": "0.33.1",
|
|
76
|
+
"@meith/profile-fields": "0.33.1",
|
|
77
|
+
"@meith/relations": "0.33.1",
|
|
78
|
+
"@meith/reputation": "0.33.1",
|
|
79
|
+
"@meith/runtime": "0.33.1",
|
|
80
|
+
"@meith/search": "0.33.1",
|
|
81
|
+
"@meith/settings": "0.33.1",
|
|
82
|
+
"@meith/signatures": "0.33.1",
|
|
83
|
+
"@meith/subscriptions": "0.33.1",
|
|
84
|
+
"@meith/tasks": "0.33.1",
|
|
85
|
+
"@meith/theme-clubhouse": "0.33.1",
|
|
86
|
+
"@meith/theme-default": "0.33.1",
|
|
87
|
+
"@meith/theme-kit": "0.33.1",
|
|
88
|
+
"@meith/theme-midnight": "0.33.1",
|
|
89
|
+
"@meith/theme-phasebook": "0.33.1",
|
|
90
|
+
"@meith/theme-raidframe": "0.33.1",
|
|
91
|
+
"@meith/threads": "0.33.1",
|
|
92
|
+
"@meith/ui": "0.33.1",
|
|
93
|
+
"@meith/upgrade": "0.33.1"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"dev": "next dev",
|
|
@@ -507,7 +507,7 @@ export function ThemeEditorForm({
|
|
|
507
507
|
<p className="text-xs text-muted-foreground">
|
|
508
508
|
{fromCopy(copy, 'adminTheme.validated.blurb')}
|
|
509
509
|
</p>
|
|
510
|
-
<style
|
|
510
|
+
<style>{state.preview}</style>
|
|
511
511
|
<ValidatedSample copy={copy} />
|
|
512
512
|
</section>
|
|
513
513
|
)}
|
|
@@ -46,6 +46,31 @@ export function sniff(bytes: Uint8Array): ImageFormat | null {
|
|
|
46
46
|
return null
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const DANGEROUS_TAG = /<\s*(script|foreignobject|iframe|embed|object|set|animate\w*)\b/i
|
|
50
|
+
const EVENT_HANDLER_OR_SCRIPT_SCHEME = /\son\w+\s*=|javascript:|vbscript:/i
|
|
51
|
+
const REFERENCING_TAG = /<\s*(use|image|feimage)\b([^>]*)>/gi
|
|
52
|
+
const HREF_ATTRIBUTE = /(?:xlink:href|href)\s*=\s*(\x22|\x27)([\s\S]*?)\1/i
|
|
53
|
+
|
|
54
|
+
function decodeEntities(value: string): string {
|
|
55
|
+
return value.replace(/&(#\d+|#x[0-9a-f]+|amp|lt|gt|quot|apos);/gi, (match, entity: string) => {
|
|
56
|
+
if (entity.startsWith('#')) {
|
|
57
|
+
const isHex = entity[1] === 'x' || entity[1] === 'X'
|
|
58
|
+
const code = Number.parseInt(entity.slice(isHex ? 2 : 1), isHex ? 16 : 10)
|
|
59
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : match
|
|
60
|
+
}
|
|
61
|
+
const named: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" }
|
|
62
|
+
return named[entity.toLowerCase()] ?? match
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasExternalReference(decoded: string): boolean {
|
|
67
|
+
for (const match of decoded.matchAll(REFERENCING_TAG)) {
|
|
68
|
+
const href = HREF_ATTRIBUTE.exec(match[2] ?? '')?.[2]?.trim()
|
|
69
|
+
if (href !== undefined && href !== '' && !href.startsWith('#')) return true
|
|
70
|
+
}
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
function isSvg(bytes: Uint8Array): boolean {
|
|
50
75
|
const decoder = new TextDecoder('utf-8', { fatal: false })
|
|
51
76
|
const head = decoder
|
|
@@ -56,9 +81,14 @@ function isSvg(bytes: Uint8Array): boolean {
|
|
|
56
81
|
const looksSvg = head.startsWith('<svg') || /^<\?xml[\s\S]{0,512}?<svg/i.test(head)
|
|
57
82
|
if (!looksSvg) return false
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
const decoded = decodeEntities(decoder.decode(bytes))
|
|
85
|
+
|
|
86
|
+
if (DANGEROUS_TAG.test(decoded) || EVENT_HANDLER_OR_SCRIPT_SCHEME.test(decoded)) {
|
|
60
87
|
throw new ValidationError(msg('error.app.svg-contains-script-event-handler'))
|
|
61
88
|
}
|
|
89
|
+
if (hasExternalReference(decoded)) {
|
|
90
|
+
throw new ValidationError(msg('error.app.svg-contains-external-reference'))
|
|
91
|
+
}
|
|
62
92
|
return true
|
|
63
93
|
}
|
|
64
94
|
|
|
@@ -50,6 +50,7 @@ export interface SystemHealthView {
|
|
|
50
50
|
readonly recount: readonly RecountStateRow[]
|
|
51
51
|
readonly volumes: BoardVolumes
|
|
52
52
|
readonly prunableSessions: number
|
|
53
|
+
readonly legacyPasswordHashes: number
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export async function buildSystemHealthView(now: Date): Promise<SystemHealthView | null> {
|
|
@@ -57,15 +58,17 @@ export async function buildSystemHealthView(now: Date): Promise<SystemHealthView
|
|
|
57
58
|
if (repository === null) return null
|
|
58
59
|
|
|
59
60
|
const maintenance = new PostgresMaintenanceRepository(getDb())
|
|
60
|
-
const [tasks, runs, recount, volumes, prunableSessions, searchIndex, mail] =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
const [tasks, runs, recount, volumes, prunableSessions, searchIndex, mail, legacyPasswordHashes] =
|
|
62
|
+
await Promise.all([
|
|
63
|
+
repository.taskHealth(),
|
|
64
|
+
repository.recentRuns(20),
|
|
65
|
+
repository.recountState(),
|
|
66
|
+
repository.volumes(),
|
|
67
|
+
maintenance.countPrunableSessions(now),
|
|
68
|
+
new PostgresSearchRepository(getDb()).indexProgress(),
|
|
69
|
+
assessMailReadiness(),
|
|
70
|
+
repository.legacyPasswordHashes(),
|
|
71
|
+
])
|
|
69
72
|
|
|
70
73
|
const scheduler = assessScheduler(tasks, now)
|
|
71
74
|
const taskMessages = new Map(
|
|
@@ -86,5 +89,6 @@ export async function buildSystemHealthView(now: Date): Promise<SystemHealthView
|
|
|
86
89
|
recount,
|
|
87
90
|
volumes,
|
|
88
91
|
prunableSessions,
|
|
92
|
+
legacyPasswordHashes,
|
|
89
93
|
}
|
|
90
94
|
}
|
|
@@ -16,7 +16,7 @@ import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
|
|
|
16
16
|
|
|
17
17
|
import { activeDefinitions } from './plugin-host'
|
|
18
18
|
|
|
19
|
-
export const CODE_VERSION = '0.33.
|
|
19
|
+
export const CODE_VERSION = '0.33.1'
|
|
20
20
|
|
|
21
21
|
export interface UpgradeApplied {
|
|
22
22
|
readonly plugins: readonly string[]
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
import type { Translator } from '@meith/i18n'
|
|
1
2
|
import type { RedirectNoticeModel } from '@meith/theme-kit'
|
|
2
3
|
|
|
4
|
+
import { untranslated } from './time'
|
|
5
|
+
|
|
3
6
|
export const REDIRECT_DELAY_SECONDS = 2
|
|
4
7
|
|
|
8
|
+
const DEFAULT_MESSAGE_KEY = 'redirectNotice.continuing'
|
|
9
|
+
|
|
10
|
+
const REDIRECT_MESSAGE_KEYS: Readonly<Record<string, string>> = {
|
|
11
|
+
continuing: DEFAULT_MESSAGE_KEY,
|
|
12
|
+
'signed-out': 'redirectNotice.signedOut',
|
|
13
|
+
saved: 'redirectNotice.saved',
|
|
14
|
+
posted: 'redirectNotice.posted',
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
export function localHref(value: string | undefined): string {
|
|
6
18
|
if (value === undefined || !value.startsWith('/')) return '/'
|
|
7
19
|
const origin = 'https://forum.invalid'
|
|
@@ -11,11 +23,16 @@ export function localHref(value: string | undefined): string {
|
|
|
11
23
|
|
|
12
24
|
export function buildRedirectNotice(
|
|
13
25
|
target: string | undefined,
|
|
14
|
-
|
|
26
|
+
messageKey: string | undefined,
|
|
27
|
+
t: Translator = untranslated(),
|
|
15
28
|
): RedirectNoticeModel {
|
|
29
|
+
const key =
|
|
30
|
+
(messageKey !== undefined ? REDIRECT_MESSAGE_KEYS[messageKey] : undefined) ??
|
|
31
|
+
DEFAULT_MESSAGE_KEY
|
|
32
|
+
|
|
16
33
|
return {
|
|
17
34
|
targetHref: localHref(target),
|
|
18
|
-
message:
|
|
35
|
+
message: t.t(key),
|
|
19
36
|
delaySeconds: REDIRECT_DELAY_SECONDS,
|
|
20
37
|
}
|
|
21
38
|
}
|