@meith/web 0.35.0 → 0.36.0
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/(board)/member/[id]/reputation/page.tsx +2 -4
- package/app/(board)/plugins/[key]/[[...path]]/page.tsx +2 -1
- package/app/(board)/search/page.tsx +2 -1
- package/app/(board)/unsubscribe/page.tsx +2 -2
- package/app/admin/layout.tsx +9 -1
- package/app/admin/plugins/[key]/[[...path]]/page.tsx +1 -3
- package/app/auth/passkey/verify/route.ts +1 -2
- package/app/install/page.tsx +2 -4
- package/app/layout.tsx +2 -0
- package/package.json +50 -50
- package/src/components/account/active-sessions.tsx +2 -2
- package/src/components/account/avatar-form.tsx +3 -2
- package/src/components/account/feed-token.tsx +5 -5
- package/src/components/account/notification-forms.tsx +4 -6
- package/src/components/account/push-device-form.tsx +3 -2
- package/src/components/account/reputation-forms.tsx +5 -4
- package/src/components/account/sign-in-methods.tsx +2 -2
- package/src/components/account/subscription-forms.tsx +6 -8
- package/src/components/account/two-factor-forms.tsx +3 -4
- package/src/components/account/usercp-forms.tsx +48 -59
- package/src/components/admin/admin-forms.tsx +27 -18
- package/src/components/admin/form-bits.tsx +2 -3
- package/src/components/admin/mail-test-card.tsx +3 -1
- package/src/components/admin/theme-forms.tsx +4 -5
- package/src/components/auth/form-controls.tsx +22 -11
- package/src/components/content/edit-post-form.tsx +3 -2
- package/src/components/content/new-thread-form.tsx +4 -4
- package/src/components/messages/message-forms.tsx +9 -10
- package/src/components/moderation/inline-moderation-form.tsx +3 -2
- package/src/components/moderation/queue-form.tsx +3 -3
- package/src/components/moderation/report-forms.tsx +3 -3
- package/src/components/moderation/thread-surgery-form.tsx +4 -4
- package/src/components/moderation/thread-tools-form.tsx +4 -4
- package/src/components/moderation/warning-forms.tsx +8 -8
- package/src/components/shell/install-action.tsx +3 -2
- package/src/components/shell/nav-disclosure-enhancer.tsx +3 -0
- package/src/components/shell/scheme-toggle.tsx +1 -1
- package/src/components/shell/theme-switcher.tsx +9 -8
- package/src/components/shell/view-tabs.tsx +1 -65
- package/src/server/admin-actions.ts +30 -21
- package/src/server/auth-actions.ts +11 -4
- package/src/server/federation.ts +3 -1
- package/src/server/upgrade-notice.ts +1 -1
- package/src/styles/globals.css +63 -0
- package/src/view/copy.ts +2 -0
- package/src/view/panel-nav.ts +8 -10
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useActionState } from 'react'
|
|
4
4
|
|
|
5
|
+
import { buttonVariants, controlVariants } from '@meith/ui'
|
|
6
|
+
|
|
5
7
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
6
8
|
import { threadToolAction } from '@/server/thread-tool-actions'
|
|
7
9
|
|
|
@@ -9,10 +11,8 @@ import { FormError, PendingButton } from '../auth/form-controls'
|
|
|
9
11
|
import { ConfirmDialog } from '../shell/confirm-dialog'
|
|
10
12
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
11
13
|
|
|
12
|
-
const BUTTON =
|
|
13
|
-
|
|
14
|
-
const SELECT =
|
|
15
|
-
'h-8 min-w-0 max-w-full rounded-md border border-border bg-background px-2 text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
14
|
+
const BUTTON = buttonVariants({ variant: 'outline', size: 'sm' })
|
|
15
|
+
const SELECT = controlVariants({ size: 'sm', className: 'w-auto max-w-full' })
|
|
16
16
|
const SUMMARY =
|
|
17
17
|
'flex cursor-pointer list-none items-center gap-2 rounded-lg px-4 py-3 text-xs font-medium text-muted-foreground select-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden'
|
|
18
18
|
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import { useActionState } from 'react'
|
|
4
4
|
|
|
5
|
+
import { buttonVariants, controlVariants } from '@meith/ui'
|
|
6
|
+
|
|
5
7
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
6
8
|
import { issueWarningAction, revokeWarningAction } from '@/server/warning-actions'
|
|
7
9
|
|
|
8
10
|
import { FormError, PendingButton } from '../auth/form-controls'
|
|
9
11
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
10
12
|
|
|
11
|
-
const FIELD =
|
|
12
|
-
'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
13
|
+
const FIELD = controlVariants()
|
|
13
14
|
|
|
14
|
-
const BUTTON =
|
|
15
|
-
'inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
15
|
+
const BUTTON = buttonVariants({ variant: 'primary', size: 'default' })
|
|
16
16
|
|
|
17
17
|
export interface WarningTypeOption {
|
|
18
18
|
readonly id: number
|
|
@@ -45,7 +45,7 @@ export function IssueWarningForm({
|
|
|
45
45
|
<input type="hidden" name="userId" value={userId} />
|
|
46
46
|
{postId !== null && <input type="hidden" name="postId" value={postId} />}
|
|
47
47
|
|
|
48
|
-
<label className="flex flex-col gap-
|
|
48
|
+
<label className="flex min-w-0 flex-col gap-2 text-sm">
|
|
49
49
|
<span className="font-medium">{fromCopy(copy, 'moderationForm.warn.reason')}</span>
|
|
50
50
|
<select name="typeId" className={FIELD} defaultValue="">
|
|
51
51
|
<option value="">{fromCopy(copy, 'moderationForm.warn.somethingElse')}</option>
|
|
@@ -58,20 +58,20 @@ export function IssueWarningForm({
|
|
|
58
58
|
</label>
|
|
59
59
|
|
|
60
60
|
<div className="grid gap-4 sm:grid-cols-[1fr_8rem]">
|
|
61
|
-
<label className="flex flex-col gap-
|
|
61
|
+
<label className="flex min-w-0 flex-col gap-2 text-sm">
|
|
62
62
|
<span className="font-medium">{fromCopy(copy, 'moderationForm.warn.titleLabel')}</span>
|
|
63
63
|
<input type="text" name="title" maxLength={150} className={FIELD} />
|
|
64
64
|
<span className="text-xs text-muted-foreground">
|
|
65
65
|
{fromCopy(copy, 'moderationForm.warn.titleHint')}
|
|
66
66
|
</span>
|
|
67
67
|
</label>
|
|
68
|
-
<label className="flex flex-col gap-
|
|
68
|
+
<label className="flex min-w-0 flex-col gap-2 text-sm">
|
|
69
69
|
<span className="font-medium">{fromCopy(copy, 'moderationForm.warn.points')}</span>
|
|
70
70
|
<input type="number" name="points" min={1} max={100} className={FIELD} />
|
|
71
71
|
</label>
|
|
72
72
|
</div>
|
|
73
73
|
|
|
74
|
-
<label className="flex flex-col gap-
|
|
74
|
+
<label className="flex min-w-0 flex-col gap-2 text-sm">
|
|
75
75
|
<span className="font-medium">{fromCopy(copy, 'moderationForm.warn.what')}</span>
|
|
76
76
|
<textarea name="reason" rows={4} maxLength={2000} className={FIELD} required />
|
|
77
77
|
<span className="text-xs text-muted-foreground">
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
4
|
|
|
5
|
+
import { buttonVariants } from '@meith/ui'
|
|
6
|
+
|
|
5
7
|
import { INSTALL_BANNER_COOKIE, INSTALL_BANNER_COOKIE_MAX_AGE } from '@/view/install-banner'
|
|
6
8
|
|
|
7
9
|
interface InstallPrompt extends Event {
|
|
@@ -23,8 +25,7 @@ function isInstallPrompt(event: Event): event is InstallPrompt {
|
|
|
23
25
|
return 'prompt' in event && 'userChoice' in event
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const BUTTON =
|
|
27
|
-
'inline-flex h-8 items-center rounded-md border border-border bg-background px-3 text-xs font-medium text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
28
|
+
const BUTTON = buttonVariants({ variant: 'outline', size: 'sm' })
|
|
28
29
|
|
|
29
30
|
export function InstallAction({ label, how }: { label: string; how: string }) {
|
|
30
31
|
const [pending, setPending] = useState<InstallPrompt | null>(null)
|
|
@@ -19,6 +19,9 @@ export function NavDisclosureEnhancer() {
|
|
|
19
19
|
if (event.key !== 'Escape') return
|
|
20
20
|
|
|
21
21
|
for (const details of document.querySelectorAll<HTMLDetailsElement>(OPEN_DISCLOSURES)) {
|
|
22
|
+
if (details.contains(document.activeElement)) {
|
|
23
|
+
details.querySelector('summary')?.focus()
|
|
24
|
+
}
|
|
22
25
|
details.open = false
|
|
23
26
|
}
|
|
24
27
|
}
|
|
@@ -55,7 +55,7 @@ export function SchemeToggle({
|
|
|
55
55
|
title={labels[option]}
|
|
56
56
|
aria-pressed={selected}
|
|
57
57
|
onClick={() => applySchemeImmediately(option)}
|
|
58
|
-
className={`inline-flex h-8 items-center justify-center border-border px-2.5 transition-colors [&:not(:first-child)]:border-l focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring ${
|
|
58
|
+
className={`inline-flex h-8 pointer-coarse:min-h-11 pointer-coarse:min-w-11 items-center justify-center border-border px-2.5 transition-colors [&:not(:first-child)]:border-l focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring ${
|
|
59
59
|
selected
|
|
60
60
|
? 'bg-primary text-primary-foreground'
|
|
61
61
|
: 'bg-background hover:bg-accent hover:text-accent-foreground'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buttonVariants, cn, controlVariants } from '@meith/ui'
|
|
2
|
+
|
|
1
3
|
import { PendingButton } from '@/components/auth/form-controls'
|
|
2
4
|
import { MonitorIcon, MoonIcon, SunIcon } from '@/components/shell/appearance-icons'
|
|
3
5
|
import { SchemeToggle } from '@/components/shell/scheme-toggle'
|
|
@@ -13,9 +15,6 @@ const SCHEME_LABEL_KEY: Record<ColourSchemePreference, string> = {
|
|
|
13
15
|
dark: 'appearance.scheme.dark',
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
const CONTROL =
|
|
17
|
-
'h-8 rounded-md border border-border bg-background px-2 text-xs text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
18
|
-
|
|
19
18
|
export async function ThemeSwitcher() {
|
|
20
19
|
const [{ choices }, theme, scheme, t] = await Promise.all([
|
|
21
20
|
getBoardThemeStyle(),
|
|
@@ -43,17 +42,19 @@ export async function ThemeSwitcher() {
|
|
|
43
42
|
<label htmlFor="appearance-theme" className="text-xs text-muted-foreground">
|
|
44
43
|
{t.t('appearance.theme')}
|
|
45
44
|
</label>
|
|
46
|
-
<select
|
|
45
|
+
<select
|
|
46
|
+
id="appearance-theme"
|
|
47
|
+
name="theme"
|
|
48
|
+
defaultValue={theme}
|
|
49
|
+
className={cn(controlVariants({ size: 'sm' }), 'w-auto')}
|
|
50
|
+
>
|
|
47
51
|
{choices.map((choice) => (
|
|
48
52
|
<option key={choice.key} value={choice.key}>
|
|
49
53
|
{choice.title}
|
|
50
54
|
</option>
|
|
51
55
|
))}
|
|
52
56
|
</select>
|
|
53
|
-
<PendingButton
|
|
54
|
-
showWorking
|
|
55
|
-
className="h-8 rounded-md border border-border px-2.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
56
|
-
>
|
|
57
|
+
<PendingButton showWorking className={buttonVariants({ variant: 'outline', size: 'sm' })}>
|
|
57
58
|
{t.t('appearance.apply')}
|
|
58
59
|
</PendingButton>
|
|
59
60
|
</form>
|
|
@@ -1,65 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export interface ViewTab {
|
|
4
|
-
readonly href: string
|
|
5
|
-
readonly label: string
|
|
6
|
-
readonly isCurrent: boolean
|
|
7
|
-
readonly count?: number
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function ViewTabs({
|
|
11
|
-
label,
|
|
12
|
-
tabs,
|
|
13
|
-
aside,
|
|
14
|
-
className,
|
|
15
|
-
}: {
|
|
16
|
-
label: string
|
|
17
|
-
tabs: readonly ViewTab[]
|
|
18
|
-
aside?: React.ReactNode
|
|
19
|
-
className?: string
|
|
20
|
-
}) {
|
|
21
|
-
if (tabs.length === 0) return null
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<nav
|
|
25
|
-
aria-label={label}
|
|
26
|
-
className={cn('flex flex-wrap items-center gap-x-3 gap-y-2', className)}
|
|
27
|
-
>
|
|
28
|
-
<ul className="inline-flex max-w-full items-center gap-1 overflow-x-auto rounded-lg border border-border bg-surface p-1">
|
|
29
|
-
{tabs.map((tab) => (
|
|
30
|
-
<li key={tab.href} className="shrink-0">
|
|
31
|
-
<a
|
|
32
|
-
href={tab.href}
|
|
33
|
-
{...(tab.isCurrent ? { 'aria-current': 'page' as const } : {})}
|
|
34
|
-
className={cn(
|
|
35
|
-
'inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-sm whitespace-nowrap transition-colors',
|
|
36
|
-
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
|
|
37
|
-
tab.isCurrent
|
|
38
|
-
? 'bg-card font-semibold text-primary shadow-sm'
|
|
39
|
-
: 'font-medium text-muted-foreground hover:text-foreground',
|
|
40
|
-
)}
|
|
41
|
-
>
|
|
42
|
-
{tab.label}
|
|
43
|
-
{tab.count !== undefined && tab.count > 0 && (
|
|
44
|
-
<span
|
|
45
|
-
className={cn(
|
|
46
|
-
'rounded px-1.5 text-xs font-semibold tabular-nums',
|
|
47
|
-
tab.isCurrent
|
|
48
|
-
? 'bg-secondary text-secondary-foreground'
|
|
49
|
-
: 'bg-muted text-muted-foreground',
|
|
50
|
-
)}
|
|
51
|
-
>
|
|
52
|
-
{tab.count}
|
|
53
|
-
</span>
|
|
54
|
-
)}
|
|
55
|
-
</a>
|
|
56
|
-
</li>
|
|
57
|
-
))}
|
|
58
|
-
</ul>
|
|
59
|
-
|
|
60
|
-
{aside !== undefined && (
|
|
61
|
-
<span className="shrink-0 text-xs text-muted-foreground">{aside}</span>
|
|
62
|
-
)}
|
|
63
|
-
</nav>
|
|
64
|
-
)
|
|
65
|
-
}
|
|
1
|
+
export { type NavTab as ViewTab, NavTabs as ViewTabs } from '@meith/ui'
|
|
@@ -55,28 +55,33 @@ export async function adminSignInAction(_prev: FormState, form: FormData): Promi
|
|
|
55
55
|
const config = await boardAuthConfig()
|
|
56
56
|
const attemptBucket = await adminReauthenticationBucket(actor.userId)
|
|
57
57
|
const attemptSince = new Date(Date.now() - config.lockoutMinutes * 60_000)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
|
|
59
|
+
let attemptId: number | null = null
|
|
60
|
+
if (config.maxLoginAttempts > 0) {
|
|
61
|
+
const spent = await accountStore.loginAttempts.recordFailureAndCount(
|
|
62
|
+
attemptBucket,
|
|
63
|
+
attemptSince,
|
|
64
|
+
new Date(),
|
|
65
|
+
)
|
|
66
|
+
attemptId = spent.id
|
|
67
|
+
if (spent.count > config.maxLoginAttempts) {
|
|
68
|
+
throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
|
|
69
|
+
}
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
const
|
|
67
|
-
await accountStore.loginAttempts.record(attemptBucket, false, new Date())
|
|
72
|
+
const auditFailure = async (): Promise<void> => {
|
|
68
73
|
await recordAdminAction({ action: 'admin.signin_failed' })
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
const password = text(form, 'password')
|
|
72
77
|
if (password === '') {
|
|
73
|
-
await
|
|
78
|
+
await auditFailure()
|
|
74
79
|
throw new ValidationError(msg('error.app.enter-password'))
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
const ok = await verifyPassword(password, account.passwordHash)
|
|
78
83
|
if (!ok) {
|
|
79
|
-
await
|
|
84
|
+
await auditFailure()
|
|
80
85
|
throw new ForbiddenError(msg('error.app.password-right'))
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -84,11 +89,12 @@ export async function adminSignInAction(_prev: FormState, form: FormData): Promi
|
|
|
84
89
|
const twoFactor = twoFactorService()
|
|
85
90
|
|
|
86
91
|
if (twoFactor !== null && (await twoFactor.isEnrolled(actor.userId))) {
|
|
92
|
+
if (attemptId !== null) await accountStore.loginAttempts.removeAttempt(attemptId)
|
|
87
93
|
await holdAdminSecondFactor(actor.userId, next)
|
|
88
94
|
target = '/admin'
|
|
89
95
|
} else {
|
|
90
96
|
if (twoFactor !== null && (await twoFactorRequiredForStaff())) {
|
|
91
|
-
await
|
|
97
|
+
await auditFailure()
|
|
92
98
|
throw new ForbiddenError(msg('adminAction.twoFactorRequired'))
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -133,28 +139,31 @@ export async function adminVerifySecondFactorAction(
|
|
|
133
139
|
const config = await boardAuthConfig()
|
|
134
140
|
const attemptBucket = await adminReauthenticationBucket(actor.userId)
|
|
135
141
|
const attemptSince = new Date(Date.now() - config.lockoutMinutes * 60_000)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
|
|
143
|
+
if (config.maxLoginAttempts > 0) {
|
|
144
|
+
const spent = await accountStore.loginAttempts.recordFailureAndCount(
|
|
145
|
+
attemptBucket,
|
|
146
|
+
attemptSince,
|
|
147
|
+
new Date(),
|
|
148
|
+
)
|
|
149
|
+
if (spent.count > config.maxLoginAttempts) {
|
|
150
|
+
throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
|
|
151
|
+
}
|
|
142
152
|
}
|
|
143
153
|
|
|
144
|
-
const
|
|
145
|
-
await accountStore.loginAttempts.record(attemptBucket, false, new Date())
|
|
154
|
+
const auditFailure = async (): Promise<void> => {
|
|
146
155
|
await recordAdminAction({ action: 'admin.signin_failed' })
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
const code = text(form, 'code')
|
|
150
159
|
if (code === '') {
|
|
151
|
-
await
|
|
160
|
+
await auditFailure()
|
|
152
161
|
throw new ValidationError(msg('error.app.enter-code-from-authenticator-app'))
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
const outcome = await twoFactor.verify({ userId: pending.userId, code })
|
|
156
165
|
if (outcome.status !== 'ok') {
|
|
157
|
-
await
|
|
166
|
+
await auditFailure()
|
|
158
167
|
throw new ForbiddenError(
|
|
159
168
|
msg(outcome.status === 'replayed' ? 'adminAction.codeReplayed' : 'adminAction.codeWrong'),
|
|
160
169
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use server'
|
|
2
2
|
|
|
3
3
|
import { redirect } from 'next/navigation'
|
|
4
|
+
import { after } from 'next/server'
|
|
4
5
|
|
|
5
6
|
import { type AuthConfig, foldIdentifier, hashToken, type LoginBucket } from '@meith/accounts'
|
|
6
7
|
import { env, logger } from '@meith/core'
|
|
@@ -344,12 +345,11 @@ export async function verifySecondFactorAction(
|
|
|
344
345
|
if (service === null) return { error: await tr('authAction.secondFactorExpired') }
|
|
345
346
|
|
|
346
347
|
try {
|
|
347
|
-
await identity.
|
|
348
|
+
await identity.spendSecondFactorAttempt(pending.userId)
|
|
348
349
|
|
|
349
350
|
const outcome = await service.verify({ userId: pending.userId, code })
|
|
350
351
|
|
|
351
352
|
if (outcome.status !== 'ok') {
|
|
352
|
-
await identity.recordSecondFactorFailure(pending.userId)
|
|
353
353
|
await recordAuthEvent({ userId: pending.userId, kind: 'second_factor_failed' })
|
|
354
354
|
return {
|
|
355
355
|
error: await tr(
|
|
@@ -440,9 +440,10 @@ export async function requestResetAction(_prev: FormState, form: FormData): Prom
|
|
|
440
440
|
const { token, userId } = await identity.requestPasswordReset(email)
|
|
441
441
|
|
|
442
442
|
if (token !== null && userId !== null) {
|
|
443
|
-
const
|
|
444
|
-
if (account !== null) {
|
|
443
|
+
const deliver = async (): Promise<void> => {
|
|
445
444
|
try {
|
|
445
|
+
const account = await getContainer().accountStore.accounts.findById(userId)
|
|
446
|
+
if (account === null) return
|
|
446
447
|
await sendPasswordResetEmail({
|
|
447
448
|
token,
|
|
448
449
|
email: account.email,
|
|
@@ -456,6 +457,12 @@ export async function requestResetAction(_prev: FormState, form: FormData): Prom
|
|
|
456
457
|
)
|
|
457
458
|
}
|
|
458
459
|
}
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
after(deliver)
|
|
463
|
+
} catch {
|
|
464
|
+
await deliver()
|
|
465
|
+
}
|
|
459
466
|
}
|
|
460
467
|
|
|
461
468
|
if (token && env.NODE_ENV === 'development') {
|
package/src/server/federation.ts
CHANGED
|
@@ -54,7 +54,9 @@ export async function federationProvider(id: string): Promise<IdentityProvider |
|
|
|
54
54
|
if (!isProviderKind(id)) return null
|
|
55
55
|
if (getContainer().dataSource !== 'postgres') return null
|
|
56
56
|
|
|
57
|
-
return providerFor(id, federationOptions(await getSettingsUncached())
|
|
57
|
+
return providerFor(id, federationOptions(await getSettingsUncached()), {
|
|
58
|
+
allowPrivateHosts: env.OIDC_ALLOW_PRIVATE_HOSTS,
|
|
59
|
+
})
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export async function signInProviders(): Promise<readonly ProviderButton[]> {
|
|
@@ -17,7 +17,7 @@ import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
|
|
|
17
17
|
|
|
18
18
|
import { activeDefinitions } from './plugin-host'
|
|
19
19
|
|
|
20
|
-
export const CODE_VERSION = '0.
|
|
20
|
+
export const CODE_VERSION = '0.36.0'
|
|
21
21
|
|
|
22
22
|
export interface UpgradeApplied {
|
|
23
23
|
readonly plugins: readonly string[]
|
package/src/styles/globals.css
CHANGED
|
@@ -436,7 +436,12 @@
|
|
|
436
436
|
border-color: var(--border);
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
html {
|
|
440
|
+
scrollbar-gutter: stable;
|
|
441
|
+
}
|
|
442
|
+
|
|
439
443
|
body {
|
|
444
|
+
-webkit-font-smoothing: antialiased;
|
|
440
445
|
background-color: var(--background);
|
|
441
446
|
color: var(--foreground);
|
|
442
447
|
}
|
|
@@ -465,6 +470,7 @@
|
|
|
465
470
|
h3,
|
|
466
471
|
h4 {
|
|
467
472
|
text-wrap: balance;
|
|
473
|
+
overflow-wrap: anywhere;
|
|
468
474
|
}
|
|
469
475
|
|
|
470
476
|
/*
|
|
@@ -1000,3 +1006,60 @@ select:not([multiple], [size]):disabled {
|
|
|
1000
1006
|
font-size: 1rem;
|
|
1001
1007
|
}
|
|
1002
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
[data-slot="navigation-drawer"] {
|
|
1011
|
+
translate: 100% 0;
|
|
1012
|
+
transition:
|
|
1013
|
+
translate 180ms ease-out,
|
|
1014
|
+
display 180ms allow-discrete,
|
|
1015
|
+
overlay 180ms allow-discrete;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
[dir="rtl"] [data-slot="navigation-drawer"] {
|
|
1019
|
+
translate: -100% 0;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
[data-slot="navigation-drawer"]:popover-open {
|
|
1023
|
+
translate: 0 0;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
@starting-style {
|
|
1027
|
+
[data-slot="navigation-drawer"]:popover-open {
|
|
1028
|
+
translate: 100% 0;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
[dir="rtl"] [data-slot="navigation-drawer"]:popover-open {
|
|
1032
|
+
translate: -100% 0;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
@media (width < 64rem) {
|
|
1037
|
+
html:has([data-slot="navigation-drawer"]:popover-open) {
|
|
1038
|
+
overflow: hidden;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
@media (width >= 64rem) {
|
|
1043
|
+
[data-slot="navigation-drawer"]:popover-open {
|
|
1044
|
+
display: none;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1049
|
+
[data-slot="navigation-drawer"] {
|
|
1050
|
+
transition: none;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
[data-panel-nav-trigger] {
|
|
1055
|
+
display: none;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
body:has(#panel-usercp-navigation) [data-panel-nav-trigger="usercp"],
|
|
1059
|
+
body:has(#panel-modcp-navigation) [data-panel-nav-trigger="modcp"] {
|
|
1060
|
+
display: inline-flex;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
body:has(#panel-usercp-navigation, #panel-modcp-navigation) [data-main-navigation] > button {
|
|
1064
|
+
display: none;
|
|
1065
|
+
}
|
package/src/view/copy.ts
CHANGED
package/src/view/panel-nav.ts
CHANGED
|
@@ -178,16 +178,14 @@ export function buildPanelNavModel(input: {
|
|
|
178
178
|
isRecord: false,
|
|
179
179
|
isOpen,
|
|
180
180
|
isOverview: section.href === input.overviewHref,
|
|
181
|
-
children:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}))
|
|
190
|
-
: [],
|
|
181
|
+
children: visibleChildren(section, deepest).map((child) => ({
|
|
182
|
+
href: child.href,
|
|
183
|
+
title: panelText(t, child),
|
|
184
|
+
icon: null,
|
|
185
|
+
count: countFor(input.counts, child.href),
|
|
186
|
+
current: currentFor(input.location, child.href, deepest),
|
|
187
|
+
isRecord: child.record === true,
|
|
188
|
+
})),
|
|
191
189
|
}
|
|
192
190
|
})
|
|
193
191
|
|