@meith/web 0.36.1 → 0.37.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/admin/system/page.tsx +241 -164
- package/app/fixtures/page.tsx +30 -0
- package/app/layout.tsx +0 -2
- package/next.config.mjs +1 -1
- package/package.json +50 -50
- package/src/components/admin/system-run-details.tsx +72 -0
- package/src/server/auth-config.ts +2 -13
- package/src/server/board-latest.tsx +5 -3
- package/src/server/container.ts +16 -38
- package/src/server/federation-actions.ts +0 -3
- package/src/server/fixture-activity-repo.ts +127 -0
- package/src/server/fixture-content.json +3045 -0
- package/src/server/navigation.ts +6 -2
- package/src/server/plugin-routes.ts +1 -2
- package/src/server/request-fingerprint.ts +2 -15
- package/src/server/seed-board.ts +180 -29
- package/src/server/stats.ts +5 -4
- package/src/server/syndication.ts +1 -3
- package/src/server/two-factor-actions.ts +0 -3
- package/src/server/upgrade-notice.ts +1 -1
- package/src/server/user-admin-actions.ts +0 -4
- package/src/server/usercp-actions.ts +0 -5
- package/src/theme/contract.fixture.ts +6 -6
- package/src/theme/gallery.fixture.tsx +412 -0
- package/src/theme/preview-boundary.tsx +23 -0
- package/src/theme/preview.fixture.ts +304 -0
- package/src/view/system-run-detail.ts +33 -0
- package/app/demo/stripe/[[...path]]/route.ts +0 -27
- package/src/components/shell/demo-banner.tsx +0 -41
- package/src/server/demo-stripe.ts +0 -104
- package/src/server/demo-uploads.ts +0 -17
- package/src/server/demo.ts +0 -59
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Translator } from '@meith/i18n'
|
|
2
|
+
|
|
3
|
+
import { systemRunDetail } from '@/view/system-run-detail'
|
|
4
|
+
|
|
5
|
+
const FIELD_KEYS: Readonly<Record<string, string>> = {
|
|
6
|
+
attempted: 'adminSystem.result.attempted',
|
|
7
|
+
delivered: 'adminSystem.result.delivered',
|
|
8
|
+
retried: 'adminSystem.result.retried',
|
|
9
|
+
dead: 'adminSystem.result.dead',
|
|
10
|
+
relayed: 'adminSystem.result.relayed',
|
|
11
|
+
processed: 'adminSystem.result.processed',
|
|
12
|
+
removed: 'adminSystem.result.removed',
|
|
13
|
+
corrected: 'adminSystem.result.corrected',
|
|
14
|
+
flushed: 'adminSystem.result.flushed',
|
|
15
|
+
rendered: 'adminSystem.result.rendered',
|
|
16
|
+
indexed: 'adminSystem.result.indexed',
|
|
17
|
+
ok: 'adminSystem.result.ok',
|
|
18
|
+
listingCount: 'adminSystem.result.listingCount',
|
|
19
|
+
notified: 'adminSystem.result.notified',
|
|
20
|
+
promoted: 'adminSystem.result.promoted',
|
|
21
|
+
lifted: 'adminSystem.result.lifted',
|
|
22
|
+
expired: 'adminSystem.result.expired',
|
|
23
|
+
deleted: 'adminSystem.result.deleted',
|
|
24
|
+
failed: 'adminSystem.result.failed',
|
|
25
|
+
memberCount: 'adminSystem.result.memberCount',
|
|
26
|
+
online: 'adminSystem.result.online',
|
|
27
|
+
record: 'adminSystem.result.record',
|
|
28
|
+
ran: 'adminSystem.result.ran',
|
|
29
|
+
trigger: 'adminSystem.result.trigger',
|
|
30
|
+
bundle: 'adminSystem.result.bundle',
|
|
31
|
+
status: 'adminSystem.result.status',
|
|
32
|
+
skipped: 'adminSystem.result.skipped',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SystemRunDetails({
|
|
36
|
+
detail,
|
|
37
|
+
translator,
|
|
38
|
+
}: {
|
|
39
|
+
detail: string | null
|
|
40
|
+
translator: Translator
|
|
41
|
+
}) {
|
|
42
|
+
const fields = systemRunDetail(detail)
|
|
43
|
+
if (fields.length === 0) return null
|
|
44
|
+
|
|
45
|
+
const label = (key: string) => {
|
|
46
|
+
const message = Object.hasOwn(FIELD_KEYS, key) ? FIELD_KEYS[key] : undefined
|
|
47
|
+
if (message !== undefined) return translator.t(message)
|
|
48
|
+
const words = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')
|
|
49
|
+
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<dl className="mt-2 grid gap-x-6 gap-y-1 text-xs sm:grid-cols-2">
|
|
54
|
+
{fields.map((field) => (
|
|
55
|
+
<div key={JSON.stringify(field.path)} className="flex min-w-0 flex-wrap gap-x-2">
|
|
56
|
+
<dt className="text-muted-foreground">
|
|
57
|
+
{field.path.length === 0
|
|
58
|
+
? translator.t('adminSystem.result.value')
|
|
59
|
+
: field.path.map(label).join(' / ')}
|
|
60
|
+
</dt>
|
|
61
|
+
<dd className="min-w-0 break-words font-medium [overflow-wrap:anywhere]">
|
|
62
|
+
{field.value === null
|
|
63
|
+
? translator.t('adminSystem.result.empty')
|
|
64
|
+
: typeof field.value === 'boolean'
|
|
65
|
+
? translator.t(field.value ? 'adminSystem.result.yes' : 'adminSystem.result.no')
|
|
66
|
+
: String(field.value)}
|
|
67
|
+
</dd>
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
</dl>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -25,10 +25,10 @@ export async function boardAuthConfig(): Promise<AuthConfig> {
|
|
|
25
25
|
if (env.DATA_SOURCE !== 'postgres') return AUTH_CONFIG
|
|
26
26
|
|
|
27
27
|
const settings = await getSettings()
|
|
28
|
-
return
|
|
28
|
+
return {
|
|
29
29
|
...AUTH_CONFIG,
|
|
30
30
|
...resolveAuthPolicy((key) => settings.get(key as never), AUTH_CONFIG),
|
|
31
|
-
}
|
|
31
|
+
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export async function boardSessionConfig(): Promise<BoardSessionConfig> {
|
|
@@ -37,14 +37,3 @@ export async function boardSessionConfig(): Promise<BoardSessionConfig> {
|
|
|
37
37
|
sessionLifetimeDays: (await boardAuthConfig()).sessionLifetimeDays,
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
function demoOverrides(config: AuthConfig): AuthConfig {
|
|
42
|
-
if (!env.DEMO_MODE) return config
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
...config,
|
|
46
|
-
maxLoginAttempts: 50,
|
|
47
|
-
maxAccountLoginAttempts: 500,
|
|
48
|
-
lockoutMinutes: 1,
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -12,6 +12,7 @@ import { distinctUserIds } from '@/view/member-identity'
|
|
|
12
12
|
import { getContainer } from './container'
|
|
13
13
|
import { activeWordFilter } from './content-admin'
|
|
14
14
|
import { getActor } from './context'
|
|
15
|
+
import { FixtureActivityRepository } from './fixture-activity-repo'
|
|
15
16
|
import { identitiesFor } from './group-identity'
|
|
16
17
|
import { filterView, viewerRef } from './plugin-view'
|
|
17
18
|
import { currentTheme } from './theme'
|
|
@@ -21,8 +22,10 @@ export const LATEST_ROWS = 5
|
|
|
21
22
|
|
|
22
23
|
export const LATEST_REFRESH_SECONDS = 60
|
|
23
24
|
|
|
24
|
-
export function latestRepository(): PostgresLatestRepository |
|
|
25
|
-
return getContainer().dataSource === 'postgres'
|
|
25
|
+
export function latestRepository(): PostgresLatestRepository | FixtureActivityRepository {
|
|
26
|
+
return getContainer().dataSource === 'postgres'
|
|
27
|
+
? new PostgresLatestRepository(getDb())
|
|
28
|
+
: new FixtureActivityRepository()
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export async function latestScopeFor(actor: Actor): Promise<LatestScope> {
|
|
@@ -37,7 +40,6 @@ export async function latestScopeFor(actor: Actor): Promise<LatestScope> {
|
|
|
37
40
|
|
|
38
41
|
export async function renderLatestPanels(): Promise<React.ReactNode> {
|
|
39
42
|
const repo = latestRepository()
|
|
40
|
-
if (repo === null) return null
|
|
41
43
|
|
|
42
44
|
const actor = await getActor()
|
|
43
45
|
const now = new Date()
|
package/src/server/container.ts
CHANGED
|
@@ -63,7 +63,6 @@ import {
|
|
|
63
63
|
PostgresThreadWriteRepository,
|
|
64
64
|
PostgresWarningRepository,
|
|
65
65
|
} from '@meith/db'
|
|
66
|
-
import { demoResetTask } from '@meith/demo'
|
|
67
66
|
import type { DraftRepository } from '@meith/drafts'
|
|
68
67
|
import { drivers } from '@meith/drivers'
|
|
69
68
|
import { imageProcessor } from '@meith/drivers/images'
|
|
@@ -103,7 +102,6 @@ import {
|
|
|
103
102
|
REMEMBER_DAYS,
|
|
104
103
|
SESSION_LIFETIME_DAYS,
|
|
105
104
|
} from './auth-config'
|
|
106
|
-
import { clearUploadedFiles } from './demo-uploads'
|
|
107
105
|
import { FixtureActorSource } from './fixture-actor-source'
|
|
108
106
|
import { FixtureForumRepository } from './fixture-forum-repo'
|
|
109
107
|
import { FixtureMemberProfileRepository } from './fixture-member-profile-repo'
|
|
@@ -296,47 +294,27 @@ function buildPostgres(onBypass: (e: BypassEvent) => void): Container {
|
|
|
296
294
|
fixtureDataVersion: null,
|
|
297
295
|
accountStore: store,
|
|
298
296
|
...identityServices(store, new PostgresBanFilterRepository(db), new PostgresBanRepository(db)),
|
|
299
|
-
scheduler:
|
|
300
|
-
|
|
301
|
-
queue: drivers().queue,
|
|
302
|
-
db,
|
|
303
|
-
mail: drivers().mail,
|
|
304
|
-
themeKey: forumConfig.defaultTheme,
|
|
305
|
-
themeTokens: Object.fromEntries(
|
|
306
|
-
Object.values(forumConfig.themes).map((theme) => [theme.key, theme.tokens]),
|
|
307
|
-
),
|
|
308
|
-
themeVersions: Object.values(forumConfig.themes).map((theme) => ({
|
|
309
|
-
key: theme.key,
|
|
310
|
-
version: theme.theme?.version ?? null,
|
|
311
|
-
})),
|
|
312
|
-
files: drivers().files,
|
|
313
|
-
images: imageProcessor,
|
|
314
|
-
plugins: activeDefinitions(),
|
|
315
|
-
translatorForLocale,
|
|
316
|
-
}),
|
|
297
|
+
scheduler: buildSchedulerBundle({
|
|
298
|
+
queue: drivers().queue,
|
|
317
299
|
db,
|
|
318
|
-
|
|
300
|
+
mail: drivers().mail,
|
|
301
|
+
themeKey: forumConfig.defaultTheme,
|
|
302
|
+
themeTokens: Object.fromEntries(
|
|
303
|
+
Object.values(forumConfig.themes).map((theme) => [theme.key, theme.tokens]),
|
|
304
|
+
),
|
|
305
|
+
themeVersions: Object.values(forumConfig.themes).map((theme) => ({
|
|
306
|
+
key: theme.key,
|
|
307
|
+
version: theme.theme?.version ?? null,
|
|
308
|
+
})),
|
|
309
|
+
files: drivers().files,
|
|
310
|
+
images: imageProcessor,
|
|
311
|
+
plugins: activeDefinitions(),
|
|
312
|
+
translatorForLocale,
|
|
313
|
+
}),
|
|
319
314
|
dataSource: 'postgres',
|
|
320
315
|
}
|
|
321
316
|
}
|
|
322
317
|
|
|
323
|
-
function withDemoReset(bundle: SchedulerBundle, db: ReturnType<typeof getDb>): SchedulerBundle {
|
|
324
|
-
if (!env.DEMO_MODE) return bundle
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
...bundle,
|
|
328
|
-
tasks: [
|
|
329
|
-
...bundle.tasks,
|
|
330
|
-
demoResetTask({
|
|
331
|
-
db,
|
|
332
|
-
cache: drivers().cache,
|
|
333
|
-
clearUploads: () => clearUploadedFiles(),
|
|
334
|
-
plugins: activeDefinitions(),
|
|
335
|
-
}),
|
|
336
|
-
],
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
318
|
export function getContainer(): Container {
|
|
341
319
|
const g = globalThis as GlobalWithContainer
|
|
342
320
|
const cached = g[GLOBAL_KEY] as Partial<Container> | undefined
|
|
@@ -10,7 +10,6 @@ import type { FormState } from './auth-form-state'
|
|
|
10
10
|
import { getContainer } from './container'
|
|
11
11
|
import { getActor } from './context'
|
|
12
12
|
import { requireFreshCredentialProof } from './credential-proof'
|
|
13
|
-
import { assertDemoAccountChangeable } from './demo'
|
|
14
13
|
import {
|
|
15
14
|
federationService,
|
|
16
15
|
memberManagedSignIns,
|
|
@@ -53,8 +52,6 @@ async function requireOwnAccount(): Promise<{
|
|
|
53
52
|
throw new ForbiddenError(msg('error.app.board-manages-sign-ins-for-its'))
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
await assertDemoAccountChangeable(actor.userId, 'password')
|
|
57
|
-
|
|
58
55
|
const account = await getContainer().accountStore.accounts.findById(actor.userId)
|
|
59
56
|
if (account === null) throw new ForbiddenError(msg('error.app.account-longer-exists'))
|
|
60
57
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { audienceFilterIn, authorFilterAdmits } from '@meith/core'
|
|
4
|
+
import type {
|
|
5
|
+
BoardTotals,
|
|
6
|
+
LatestPostRow,
|
|
7
|
+
LatestScope,
|
|
8
|
+
LatestThreadRow,
|
|
9
|
+
TopPoster,
|
|
10
|
+
TopThread,
|
|
11
|
+
} from '@meith/db'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
SEED_FORUM_ROWS,
|
|
15
|
+
SEED_MEMBER_PROFILES,
|
|
16
|
+
SEED_POST_ROWS,
|
|
17
|
+
SEED_THREAD_ROWS,
|
|
18
|
+
} from './seed-board'
|
|
19
|
+
|
|
20
|
+
const forums = new Map(SEED_FORUM_ROWS.map((row) => [row.id, row]))
|
|
21
|
+
const threads = new Map(SEED_THREAD_ROWS.map((row) => [row.id, row]))
|
|
22
|
+
const firstPosts = new Map(
|
|
23
|
+
SEED_POST_ROWS.filter((post) => post.isFirstPost).map((post) => [post.threadId, post]),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
function visibleThreads(scope: LatestScope) {
|
|
27
|
+
return SEED_THREAD_ROWS.filter(
|
|
28
|
+
(thread) =>
|
|
29
|
+
scope.content.states.includes(thread.visibility) &&
|
|
30
|
+
authorFilterAdmits(audienceFilterIn(scope, thread.forumId), thread.authorUserId),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class FixtureActivityRepository {
|
|
35
|
+
async threads(limit: number, scope: LatestScope): Promise<readonly LatestThreadRow[]> {
|
|
36
|
+
return visibleThreads(scope)
|
|
37
|
+
.map((thread) => ({
|
|
38
|
+
threadId: thread.id,
|
|
39
|
+
title: thread.title,
|
|
40
|
+
slug: thread.slug,
|
|
41
|
+
forumId: thread.forumId,
|
|
42
|
+
forumTitle: forums.get(thread.forumId)!.title,
|
|
43
|
+
forumSlug: forums.get(thread.forumId)!.slug,
|
|
44
|
+
authorUserId: thread.authorUserId,
|
|
45
|
+
authorUsername: thread.authorUsername,
|
|
46
|
+
replyCount: thread.replyCount,
|
|
47
|
+
createdAt: firstPosts.get(thread.id)!.createdAt,
|
|
48
|
+
}))
|
|
49
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || b.threadId - a.threadId)
|
|
50
|
+
.slice(0, limit)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async posts(limit: number, scope: LatestScope): Promise<readonly LatestPostRow[]> {
|
|
54
|
+
const visible = new Set(visibleThreads(scope).map((thread) => thread.id))
|
|
55
|
+
return SEED_POST_ROWS.filter(
|
|
56
|
+
(post) => visible.has(post.threadId) && scope.content.states.includes(post.visibility),
|
|
57
|
+
)
|
|
58
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || b.id - a.id)
|
|
59
|
+
.slice(0, limit)
|
|
60
|
+
.map((post) => ({
|
|
61
|
+
postId: post.id,
|
|
62
|
+
threadId: post.threadId,
|
|
63
|
+
threadTitle: threads.get(post.threadId)!.title,
|
|
64
|
+
threadSlug: threads.get(post.threadId)!.slug,
|
|
65
|
+
forumId: post.forumId,
|
|
66
|
+
forumTitle: forums.get(post.forumId)!.title,
|
|
67
|
+
forumSlug: forums.get(post.forumId)!.slug,
|
|
68
|
+
authorUserId: post.authorUserId,
|
|
69
|
+
authorUsername: post.authorUsername,
|
|
70
|
+
createdAt: post.createdAt,
|
|
71
|
+
messageSource: post.message.slice(0, 300),
|
|
72
|
+
}))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async readTotals(): Promise<BoardTotals> {
|
|
76
|
+
const newest = [...SEED_MEMBER_PROFILES].sort(
|
|
77
|
+
(a, b) => b.createdAt.getTime() - a.createdAt.getTime() || b.id - a.id,
|
|
78
|
+
)[0]
|
|
79
|
+
return {
|
|
80
|
+
threadCount: SEED_THREAD_ROWS.length,
|
|
81
|
+
postCount: SEED_POST_ROWS.length,
|
|
82
|
+
memberCount: SEED_MEMBER_PROFILES.length,
|
|
83
|
+
newestUserId: newest?.id ?? null,
|
|
84
|
+
newestUsername: newest?.username ?? null,
|
|
85
|
+
computedAt: newest?.lastActiveAt ?? null,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async topPosters(limit: number): Promise<readonly TopPoster[]> {
|
|
90
|
+
return [...SEED_MEMBER_PROFILES]
|
|
91
|
+
.filter((member) => member.postCount > 0)
|
|
92
|
+
.sort((a, b) => b.postCount - a.postCount || a.id - b.id)
|
|
93
|
+
.slice(0, limit)
|
|
94
|
+
.map((member) => ({
|
|
95
|
+
userId: member.id,
|
|
96
|
+
username: member.username,
|
|
97
|
+
postCount: member.postCount,
|
|
98
|
+
}))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async mostViewed(limit: number, scope: LatestScope): Promise<readonly TopThread[]> {
|
|
102
|
+
return this.topThreads(limit, scope, 'viewCount')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async mostReplied(limit: number, scope: LatestScope): Promise<readonly TopThread[]> {
|
|
106
|
+
return this.topThreads(limit, scope, 'replyCount')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private topThreads(
|
|
110
|
+
limit: number,
|
|
111
|
+
scope: LatestScope,
|
|
112
|
+
key: 'viewCount' | 'replyCount',
|
|
113
|
+
): readonly TopThread[] {
|
|
114
|
+
return visibleThreads(scope)
|
|
115
|
+
.sort((a, b) => b[key] - a[key] || a.id - b.id)
|
|
116
|
+
.slice(0, limit)
|
|
117
|
+
.map((thread) => ({
|
|
118
|
+
threadId: thread.id,
|
|
119
|
+
title: thread.title,
|
|
120
|
+
slug: thread.slug,
|
|
121
|
+
forumId: thread.forumId,
|
|
122
|
+
forumTitle: forums.get(thread.forumId)!.title,
|
|
123
|
+
viewCount: thread.viewCount,
|
|
124
|
+
replyCount: thread.replyCount,
|
|
125
|
+
}))
|
|
126
|
+
}
|
|
127
|
+
}
|