@meith/web 0.17.1 → 0.17.2
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/api/marketplace/screenshot/route.ts +6 -3
- package/app/admin/forums/[id]/page.tsx +6 -3
- package/app/admin/settings/page.tsx +2 -2
- package/app/admin/system/page.tsx +1 -2
- package/app/admin/themes/page.tsx +6 -3
- package/bin/forum-web.mjs +59 -36
- package/next.config.mjs +24 -63
- package/package.json +48 -47
- package/public/placeholder-logo.png +0 -0
- package/public/placeholder-logo.svg +1 -0
- package/public/placeholder-user.jpg +0 -0
- package/public/placeholder.jpg +0 -0
- package/public/placeholder.svg +1 -0
- package/public/sw.js +81 -0
- package/src/server/admin-settings-actions.ts +8 -8
- package/src/server/upgrade-notice.ts +1 -1
- package/src/server/user-admin-actions.ts +23 -21
- package/src/server/user-admin.ts +8 -7
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { NextRequest } from 'next/server'
|
|
2
2
|
|
|
3
|
+
import { readCappedBody } from '@meith/marketplace'
|
|
4
|
+
|
|
3
5
|
import { requireAdmin } from '@/server/admin'
|
|
4
6
|
import { marketplaceScreenshotUrl } from '@/server/marketplace-admin'
|
|
5
7
|
|
|
@@ -46,13 +48,14 @@ export async function GET(request: NextRequest): Promise<Response> {
|
|
|
46
48
|
const upstream = await fetch(url, {
|
|
47
49
|
signal: controller.signal,
|
|
48
50
|
headers: { accept: 'image/png' },
|
|
51
|
+
redirect: 'manual',
|
|
49
52
|
})
|
|
50
53
|
if (!upstream.ok) return fail(502, 'The catalog host did not answer with the screenshot.')
|
|
51
54
|
|
|
52
|
-
const bytes = await upstream
|
|
53
|
-
if (bytes
|
|
55
|
+
const bytes = await readCappedBody(upstream, MAX_BYTES)
|
|
56
|
+
if (bytes === null) return fail(502, 'The screenshot was larger than expected.')
|
|
54
57
|
|
|
55
|
-
return new Response(bytes, {
|
|
58
|
+
return new Response(bytes.buffer as ArrayBuffer, {
|
|
56
59
|
headers: {
|
|
57
60
|
'Content-Type': 'image/png',
|
|
58
61
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -30,9 +30,12 @@ export default async function AdminForumPage({ params }: { params: Promise<{ id:
|
|
|
30
30
|
const options = await repository?.readOptions(forum.id)
|
|
31
31
|
if (options === null || options === undefined || repository === null) notFound()
|
|
32
32
|
|
|
33
|
-
const t = await
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const [t, moderators, groupRows] = await Promise.all([
|
|
34
|
+
getTranslator(),
|
|
35
|
+
repository.listModerators(forum.id),
|
|
36
|
+
repository.listGroups(),
|
|
37
|
+
])
|
|
38
|
+
const groups = groupRows.map((group) => ({
|
|
36
39
|
groupId: group.id,
|
|
37
40
|
title: group.title,
|
|
38
41
|
}))
|
|
@@ -31,9 +31,9 @@ export default async function AdminSettingsPage({
|
|
|
31
31
|
if (query.group === undefined && query.q === undefined) {
|
|
32
32
|
redirect(settingsHref({ group: DEFAULT_SETTING_GROUP, advanced: query.advanced === '1' }))
|
|
33
33
|
}
|
|
34
|
-
const t = await getTranslator()
|
|
34
|
+
const [t, snapshot] = await Promise.all([getTranslator(), getSettings()])
|
|
35
35
|
const model = buildAdminSettingsModel({
|
|
36
|
-
snapshot
|
|
36
|
+
snapshot,
|
|
37
37
|
query: query.q,
|
|
38
38
|
group: query.group,
|
|
39
39
|
advanced: query.advanced === '1',
|
|
@@ -36,9 +36,8 @@ export default async function AdminSystemPage() {
|
|
|
36
36
|
if ((await adminPageContext()) === null) return null
|
|
37
37
|
|
|
38
38
|
const now = new Date()
|
|
39
|
-
const translator = await getTranslator()
|
|
39
|
+
const [translator, view] = await Promise.all([getTranslator(), buildSystemHealthView(now)])
|
|
40
40
|
const copy = systemFormsCopy(translator)
|
|
41
|
-
const view = await buildSystemHealthView(now)
|
|
42
41
|
|
|
43
42
|
if (view === null) {
|
|
44
43
|
return (
|
|
@@ -24,10 +24,13 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
24
24
|
export default async function AdminThemesPage() {
|
|
25
25
|
if ((await adminPageContext()) === null) return null
|
|
26
26
|
|
|
27
|
-
const themes = await
|
|
28
|
-
|
|
27
|
+
const [themes, lightKey, darkKey, translator] = await Promise.all([
|
|
28
|
+
themeListing(),
|
|
29
|
+
logoKey('light'),
|
|
30
|
+
logoKey('dark'),
|
|
31
|
+
getTranslator(),
|
|
32
|
+
])
|
|
29
33
|
const now = new Date()
|
|
30
|
-
const translator = await getTranslator()
|
|
31
34
|
const brandingCopy = brandingFormsCopy(translator)
|
|
32
35
|
const stateCopy = themeStateCopy(translator)
|
|
33
36
|
|
package/bin/forum-web.mjs
CHANGED
|
@@ -65,24 +65,20 @@ const packageRoot = join(here, '..')
|
|
|
65
65
|
const workspaceRoot = process.cwd()
|
|
66
66
|
const appDir = join(workspaceRoot, '.meith', 'app')
|
|
67
67
|
|
|
68
|
-
// `next dev|build` runs with `.meith/app` as its own cwd (see `run()` below),
|
|
69
|
-
// so a relative FORUM_WORKSPACE_ROOT / FORUM_ALIASES_FROM — the shape
|
|
70
|
-
// boards/stock/package.json's scripts write, relative to *this* process's
|
|
71
|
-
// own cwd (the board's own directory) — would resolve against the wrong
|
|
72
|
-
// directory if left for next.config.mjs to resolve itself. Rewriting them to
|
|
73
|
-
// absolute paths here, before spawning, means both env vars mean the same
|
|
74
|
-
// thing regardless of which process reads them.
|
|
75
68
|
for (const name of ['FORUM_WORKSPACE_ROOT', 'FORUM_ALIASES_FROM']) {
|
|
76
69
|
if (process.env[name]) process.env[name] = resolve(workspaceRoot, process.env[name])
|
|
77
70
|
}
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Files this package ships (see its `files` allowlist) that belong inside the
|
|
74
|
+
* materialized app. Board files (community.config.ts, board.plugins.json,
|
|
75
|
+
* community.plugins.ts) are never copied — they are read in place, from the
|
|
76
|
+
* workspace, through the generated tsconfig instead.
|
|
77
|
+
*/
|
|
83
78
|
const APP_ENTRIES = [
|
|
84
79
|
'app',
|
|
85
80
|
'src',
|
|
81
|
+
'public',
|
|
86
82
|
'next.config.mjs',
|
|
87
83
|
'postcss.config.mjs',
|
|
88
84
|
'components.json',
|
|
@@ -136,7 +132,10 @@ function rewriteGlobalsCssSourcePaths() {
|
|
|
136
132
|
* materialized app's own generated tsconfig, rebased to be relative to
|
|
137
133
|
* `.meith/app` — the same aliases apps/community's own tsconfig.json
|
|
138
134
|
* hand-maintains for exactly this reason. Unset (the default, and the only
|
|
139
|
-
* path a real external board ever takes), this is a no-op.
|
|
135
|
+
* path a real external board ever takes), this is a no-op. `@board/config`
|
|
136
|
+
* and `@board/plugins` are excluded from the copy — they are this
|
|
137
|
+
* workspace's own seam, wired below to *this* board's files, never to
|
|
138
|
+
* whatever apps/community's own tsconfig happens to alias them to.
|
|
140
139
|
*/
|
|
141
140
|
function monorepoAliases() {
|
|
142
141
|
const configFile = process.env.FORUM_ALIASES_FROM
|
|
@@ -148,15 +147,20 @@ function monorepoAliases() {
|
|
|
148
147
|
|
|
149
148
|
const aliases = {}
|
|
150
149
|
for (const [alias, targets] of Object.entries(paths)) {
|
|
151
|
-
// `@board/config`/`@board/plugins` are this workspace's own seam, wired
|
|
152
|
-
// below to *this* board's files — never to whatever apps/community's own
|
|
153
|
-
// tsconfig happens to alias them to.
|
|
154
150
|
if (alias === '@board/config' || alias === '@board/plugins') continue
|
|
155
151
|
aliases[alias] = targets.map((target) => toPosixRelative(appDir, join(sourceDir, target)))
|
|
156
152
|
}
|
|
157
153
|
return aliases
|
|
158
154
|
}
|
|
159
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Replaces each shipped entry (`APP_ENTRIES`) but never `rm -rf`s the whole
|
|
158
|
+
* `.meith/app` directory: `.next` lives there too once a build has run, and
|
|
159
|
+
* `forum-web start` needs that build still on disk after this same function
|
|
160
|
+
* re-materializes the sources ahead of launching the standalone server. The
|
|
161
|
+
* `next-env.d.ts` it writes only needs to exist, not be complete — next
|
|
162
|
+
* regenerates it with the right content on first run.
|
|
163
|
+
*/
|
|
160
164
|
function materialize() {
|
|
161
165
|
const boardConfig = join(workspaceRoot, 'community.config.ts')
|
|
162
166
|
const boardPlugins = join(workspaceRoot, 'community.plugins.ts')
|
|
@@ -168,10 +172,6 @@ function materialize() {
|
|
|
168
172
|
)
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
// Replace each shipped entry, but never `rm -rf` the whole directory:
|
|
172
|
-
// `.next` lives here too once a build has run, and `forum-web start` needs
|
|
173
|
-
// that build still on disk after this same function re-materializes the
|
|
174
|
-
// sources ahead of launching the standalone server.
|
|
175
175
|
mkdirSync(appDir, { recursive: true })
|
|
176
176
|
|
|
177
177
|
for (const entry of APP_ENTRIES) {
|
|
@@ -213,20 +213,20 @@ function materialize() {
|
|
|
213
213
|
|
|
214
214
|
writeFileSync(join(appDir, 'tsconfig.json'), `${JSON.stringify(tsconfig, null, 2)}\n`)
|
|
215
215
|
|
|
216
|
-
// next dev/build reads its own next-env.d.ts if present; an empty one is
|
|
217
|
-
// enough, and next regenerates it with the right content on first run.
|
|
218
216
|
writeFileSync(
|
|
219
217
|
join(appDir, 'next-env.d.ts'),
|
|
220
218
|
'/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n',
|
|
221
219
|
)
|
|
222
220
|
}
|
|
223
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Resolved from this package's own directory rather than the workspace root:
|
|
224
|
+
* `next` is `@meith/web`'s dependency, not necessarily the workspace
|
|
225
|
+
* manifest's, and resolving from here finds it either way — hoisted to the
|
|
226
|
+
* workspace root (npm, yarn classic) or nested under this package's own
|
|
227
|
+
* `node_modules` (pnpm's default, non-hoisted layout).
|
|
228
|
+
*/
|
|
224
229
|
function resolveNextBin() {
|
|
225
|
-
// Resolved from this package's own directory rather than the workspace
|
|
226
|
-
// root: `next` is @meith/web's dependency, not necessarily the workspace
|
|
227
|
-
// manifest's, and resolving from here finds it either way — hoisted to
|
|
228
|
-
// the workspace root (npm, yarn classic) or nested under this package's
|
|
229
|
-
// own node_modules (pnpm's default, non-hoisted layout).
|
|
230
230
|
const require = createRequire(join(packageRoot, 'package.json'))
|
|
231
231
|
try {
|
|
232
232
|
return require.resolve('next/dist/bin/next')
|
|
@@ -235,9 +235,32 @@ function resolveNextBin() {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
function
|
|
238
|
+
function standaloneAppDir() {
|
|
239
|
+
return join(appDir, '.next', 'standalone', relative(workspaceRoot, appDir))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function stageStandaloneAssets() {
|
|
243
|
+
const targetAppDir = standaloneAppDir()
|
|
244
|
+
|
|
245
|
+
const staticTarget = join(targetAppDir, '.next', 'static')
|
|
246
|
+
rmSync(staticTarget, { recursive: true, force: true })
|
|
247
|
+
cpSync(join(appDir, '.next', 'static'), staticTarget, { recursive: true })
|
|
248
|
+
|
|
249
|
+
const publicSource = join(appDir, 'public')
|
|
250
|
+
const publicTarget = join(targetAppDir, 'public')
|
|
251
|
+
rmSync(publicTarget, { recursive: true, force: true })
|
|
252
|
+
if (existsSync(publicSource)) {
|
|
253
|
+
cpSync(publicSource, publicTarget, { recursive: true })
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function run(executable, args, cwd, onSuccess) {
|
|
239
258
|
const child = spawn(executable, args, { cwd, stdio: 'inherit' })
|
|
240
|
-
child.on('exit', (code, signal) =>
|
|
259
|
+
child.on('exit', (code, signal) => {
|
|
260
|
+
const exitCode = code ?? (signal ? 1 : 0)
|
|
261
|
+
if (exitCode === 0 && onSuccess) onSuccess()
|
|
262
|
+
process.exit(exitCode)
|
|
263
|
+
})
|
|
241
264
|
child.on('error', (error) => fail(error.message))
|
|
242
265
|
}
|
|
243
266
|
|
|
@@ -252,20 +275,20 @@ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.me
|
|
|
252
275
|
materialize()
|
|
253
276
|
|
|
254
277
|
if (command === 'start') {
|
|
255
|
-
|
|
256
|
-
// run from its own traced server.js, not `next start` (see docker/Dockerfile
|
|
257
|
-
// and docker/entrypoint.sh, which run the image's board the same way). The
|
|
258
|
-
// tracing root is the workspace root (see the module comment on why
|
|
259
|
-
// `.meith/app` sits exactly two levels below it), so the standalone bundle
|
|
260
|
-
// preserves that same relative path down to the app directory.
|
|
278
|
+
const targetAppDir = standaloneAppDir()
|
|
261
279
|
const standaloneRoot = join(appDir, '.next', 'standalone')
|
|
262
|
-
const serverScript = join(
|
|
280
|
+
const serverScript = join(targetAppDir, 'server.js')
|
|
263
281
|
if (!existsSync(serverScript)) {
|
|
264
282
|
fail(`no standalone build at ${serverScript} — run "forum-web build" first.`)
|
|
265
283
|
}
|
|
266
284
|
run(process.execPath, [serverScript, ...rest], standaloneRoot)
|
|
267
285
|
} else {
|
|
268
286
|
const nextBin = resolveNextBin()
|
|
269
|
-
run(
|
|
287
|
+
run(
|
|
288
|
+
process.execPath,
|
|
289
|
+
[nextBin, command, ...rest],
|
|
290
|
+
appDir,
|
|
291
|
+
command === 'build' ? stageStandaloneAssets : undefined,
|
|
292
|
+
)
|
|
270
293
|
}
|
|
271
294
|
}
|
package/next.config.mjs
CHANGED
|
@@ -4,35 +4,22 @@ import { fileURLToPath } from 'node:url'
|
|
|
4
4
|
|
|
5
5
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
// `boards/stock` (docker/Dockerfile) is neither: it is a workspace member of
|
|
15
|
-
// *this* monorepo's own pnpm install, which does not hoist, so its real
|
|
16
|
-
// dependencies (including `next` itself) resolve through this repository's
|
|
17
|
-
// root `node_modules`, two directories further up again — a fixed offset
|
|
18
|
-
// this file cannot compute from its own path alone, because pnpm resolves
|
|
19
|
-
// its dependencies through symlinks into a central store rather than by
|
|
20
|
-
// nesting a workspace member two directories below everything it needs.
|
|
21
|
-
// FORUM_WORKSPACE_ROOT lets the Dockerfile say so explicitly, without
|
|
22
|
-
// changing the default for every other consumer this computation already
|
|
23
|
-
// serves correctly.
|
|
7
|
+
/**
|
|
8
|
+
* Two directories up from this file's own location by default — correct at
|
|
9
|
+
* the depth every consumer places it at except `boards/stock`, which
|
|
10
|
+
* `FORUM_WORKSPACE_ROOT` overrides (see docs/architecture.md, "The stock
|
|
11
|
+
* board"; docs/development.md, "Consuming the board from a workspace").
|
|
12
|
+
*/
|
|
24
13
|
const workspaceRoot = process.env.FORUM_WORKSPACE_ROOT
|
|
25
14
|
? path.resolve(process.env.FORUM_WORKSPACE_ROOT)
|
|
26
15
|
: path.join(here, '../../')
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// not against `workspaceRoot` — an absolute glob there silently matches
|
|
35
|
-
// nothing useful once it is rejoined onto this directory downstream.
|
|
17
|
+
/**
|
|
18
|
+
* The relative equivalent of `workspaceRoot`, for `outputFileTracingIncludes`
|
|
19
|
+
* below — see docs/development.md, "Consuming the board from a workspace",
|
|
20
|
+
* for why that option needs a path relative to this file rather than an
|
|
21
|
+
* absolute one.
|
|
22
|
+
*/
|
|
36
23
|
const upToWorkspaceRoot = path.relative(here, workspaceRoot).split(path.sep).join('/')
|
|
37
24
|
|
|
38
25
|
const loadedEnvFiles = []
|
|
@@ -52,6 +39,10 @@ const nextConfig = {
|
|
|
52
39
|
poweredByHeader: false,
|
|
53
40
|
distDir: process.env.FORUM_DIST_DIR ?? '.next',
|
|
54
41
|
|
|
42
|
+
experimental: {
|
|
43
|
+
turbopackFileSystemCacheForDev: false,
|
|
44
|
+
},
|
|
45
|
+
|
|
55
46
|
serverExternalPackages: [
|
|
56
47
|
'@aws-sdk/client-s3',
|
|
57
48
|
'@aws-sdk/s3-request-presigner',
|
|
@@ -62,23 +53,11 @@ const nextConfig = {
|
|
|
62
53
|
'nodemailer',
|
|
63
54
|
],
|
|
64
55
|
outputFileTracingRoot: workspaceRoot,
|
|
65
|
-
|
|
66
|
-
// otherwise stops at this app's own directory — which for a materialized
|
|
67
|
-
// app (apps/community/bin/forum-web.mjs) is two directories below the
|
|
68
|
-
// real one. Left unset, Turbopack's own Node.js transform pool (postcss,
|
|
69
|
-
// for instance) cannot see the invoking workspace's node_modules at all,
|
|
70
|
-
// and fails with "Cannot find module" for a dependency that plain Node
|
|
71
|
-
// resolution finds without trouble.
|
|
56
|
+
/** See docs/development.md, "Consuming the board from a workspace", for why this is set. */
|
|
72
57
|
turbopack: {
|
|
73
58
|
root: workspaceRoot,
|
|
74
59
|
},
|
|
75
|
-
|
|
76
|
-
// package and misses the `esm/` variant next's own require-hook resolves at
|
|
77
|
-
// runtime, so the standalone build ships a `@swc/helpers` directory missing
|
|
78
|
-
// its esm/ half. next resolves the package from its own nested pnpm store
|
|
79
|
-
// entry (node_modules/.pnpm/next@…/node_modules/@swc/helpers), which is a
|
|
80
|
-
// symlink into node_modules/.pnpm/@swc+helpers@0.5.23/node_modules/@swc/helpers
|
|
81
|
-
// — so that is the path that has to be complete, not the app's own copy.
|
|
60
|
+
/** Works around a gap in Next's own tracing — see docs/development.md, "Consuming the board from a workspace". */
|
|
82
61
|
outputFileTracingIncludes: {
|
|
83
62
|
'/**': [
|
|
84
63
|
`${upToWorkspaceRoot}/node_modules/.pnpm/@swc+helpers@0.5.23/node_modules/@swc/helpers/**/*`,
|
|
@@ -87,24 +66,12 @@ const nextConfig = {
|
|
|
87
66
|
images: {
|
|
88
67
|
unoptimized: true,
|
|
89
68
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// seemingly arbitrary subset: only the packages some other resolution path
|
|
97
|
-
// happened to touch via node_modules ever needed it here.
|
|
98
|
-
//
|
|
99
|
-
// A materialized workspace's own generated tsconfig carries no such alias
|
|
100
|
-
// map (see apps/community/bin/forum-web.mjs) — only `@board/config` and
|
|
101
|
-
// `@board/plugins`, the seam itself. Every other `@meith/*` specifier
|
|
102
|
-
// resolves the ordinary way once this package is npm-installed, which
|
|
103
|
-
// means every one of them needs this same source-compilation treatment,
|
|
104
|
-
// or the build fails with "Unknown module type" on its `src/index.ts`.
|
|
105
|
-
// Computed by walking this app's own imports plus every `@meith/*`
|
|
106
|
-
// package's own "dependencies" transitively — see docs/development.md,
|
|
107
|
-
// "Consuming the board from a workspace".
|
|
69
|
+
/**
|
|
70
|
+
* Every `@meith/*` package this app's dependency graph reaches, not just the
|
|
71
|
+
* ones apps/community imports directly — see docs/development.md,
|
|
72
|
+
* "Consuming the board from a workspace", and docs/release.md, "They ship
|
|
73
|
+
* TypeScript source, deliberately".
|
|
74
|
+
*/
|
|
108
75
|
transpilePackages: [
|
|
109
76
|
'@meith/accounts',
|
|
110
77
|
'@meith/admin',
|
|
@@ -152,12 +119,6 @@ const nextConfig = {
|
|
|
152
119
|
'@meith/threads',
|
|
153
120
|
'@meith/ui',
|
|
154
121
|
'@meith/upgrade',
|
|
155
|
-
// Only reached through the `@meith/web/config` subpath, and only from a
|
|
156
|
-
// materialized workspace's own community.config.ts — inside this
|
|
157
|
-
// monorepo apps/community never imports itself by package name. Real
|
|
158
|
-
// once npm resolves this package into another workspace's node_modules,
|
|
159
|
-
// so it needs the same source-compilation treatment as everything else
|
|
160
|
-
// in this list.
|
|
161
122
|
'@meith/web',
|
|
162
123
|
],
|
|
163
124
|
async headers() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
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": "LGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin",
|
|
19
19
|
"app",
|
|
20
20
|
"src",
|
|
21
|
+
"public",
|
|
21
22
|
"next.config.mjs",
|
|
22
23
|
"postcss.config.mjs",
|
|
23
24
|
"components.json",
|
|
@@ -42,52 +43,52 @@
|
|
|
42
43
|
"typescript": "7.0.2",
|
|
43
44
|
"@types/react": "^19",
|
|
44
45
|
"@types/react-dom": "^19",
|
|
45
|
-
"@meith/
|
|
46
|
-
"@meith/
|
|
47
|
-
"@meith/
|
|
48
|
-
"@meith/api": "0.17.
|
|
49
|
-
"@meith/
|
|
50
|
-
"@meith/
|
|
51
|
-
"@meith/avatars": "0.17.
|
|
52
|
-
"@meith/core": "0.17.
|
|
53
|
-
"@meith/db": "0.17.
|
|
54
|
-
"@meith/
|
|
55
|
-
"@meith/
|
|
56
|
-
"@meith/
|
|
57
|
-
"@meith/
|
|
58
|
-
"@meith/
|
|
59
|
-
"@meith/
|
|
60
|
-
"@meith/
|
|
61
|
-
"@meith/
|
|
62
|
-
"@meith/
|
|
63
|
-
"@meith/markdown": "0.17.
|
|
64
|
-
"@meith/import": "0.17.
|
|
65
|
-
"@meith/
|
|
66
|
-
"@meith/
|
|
67
|
-
"@meith/notifications": "0.17.
|
|
68
|
-
"@meith/
|
|
69
|
-
"@meith/
|
|
70
|
-
"@meith/plugin-
|
|
71
|
-
"@meith/posts": "0.17.
|
|
72
|
-
"@meith/
|
|
73
|
-
"@meith/
|
|
74
|
-
"@meith/
|
|
75
|
-
"@meith/search": "0.17.
|
|
76
|
-
"@meith/
|
|
77
|
-
"@meith/
|
|
78
|
-
"@meith/
|
|
79
|
-
"@meith/
|
|
80
|
-
"@meith/subscriptions": "0.17.
|
|
81
|
-
"@meith/tasks": "0.17.
|
|
82
|
-
"@meith/theme-clubhouse": "0.17.
|
|
83
|
-
"@meith/theme-
|
|
84
|
-
"@meith/theme-
|
|
85
|
-
"@meith/theme-midnight": "0.17.
|
|
86
|
-
"@meith/theme-phasebook": "0.17.
|
|
87
|
-
"@meith/theme-raidframe": "0.17.
|
|
88
|
-
"@meith/
|
|
89
|
-
"@meith/
|
|
90
|
-
"@meith/upgrade": "0.17.
|
|
46
|
+
"@meith/admin": "0.17.2",
|
|
47
|
+
"@meith/antispam": "0.17.2",
|
|
48
|
+
"@meith/attachments": "0.17.2",
|
|
49
|
+
"@meith/api": "0.17.2",
|
|
50
|
+
"@meith/authorization": "0.17.2",
|
|
51
|
+
"@meith/accounts": "0.17.2",
|
|
52
|
+
"@meith/avatars": "0.17.2",
|
|
53
|
+
"@meith/core": "0.17.2",
|
|
54
|
+
"@meith/db": "0.17.2",
|
|
55
|
+
"@meith/events": "0.17.2",
|
|
56
|
+
"@meith/demo": "0.17.2",
|
|
57
|
+
"@meith/drivers": "0.17.2",
|
|
58
|
+
"@meith/forums": "0.17.2",
|
|
59
|
+
"@meith/groups": "0.17.2",
|
|
60
|
+
"@meith/i18n": "0.17.2",
|
|
61
|
+
"@meith/drafts": "0.17.2",
|
|
62
|
+
"@meith/mail": "0.17.2",
|
|
63
|
+
"@meith/install": "0.17.2",
|
|
64
|
+
"@meith/markdown": "0.17.2",
|
|
65
|
+
"@meith/import": "0.17.2",
|
|
66
|
+
"@meith/marketplace": "0.17.2",
|
|
67
|
+
"@meith/moderation": "0.17.2",
|
|
68
|
+
"@meith/notifications": "0.17.2",
|
|
69
|
+
"@meith/messages": "0.17.2",
|
|
70
|
+
"@meith/polls": "0.17.2",
|
|
71
|
+
"@meith/plugin-dues": "0.17.2",
|
|
72
|
+
"@meith/posts": "0.17.2",
|
|
73
|
+
"@meith/plugin-kit": "0.17.2",
|
|
74
|
+
"@meith/profile-fields": "0.17.2",
|
|
75
|
+
"@meith/runtime": "0.17.2",
|
|
76
|
+
"@meith/search": "0.17.2",
|
|
77
|
+
"@meith/relations": "0.17.2",
|
|
78
|
+
"@meith/reputation": "0.17.2",
|
|
79
|
+
"@meith/signatures": "0.17.2",
|
|
80
|
+
"@meith/settings": "0.17.2",
|
|
81
|
+
"@meith/subscriptions": "0.17.2",
|
|
82
|
+
"@meith/tasks": "0.17.2",
|
|
83
|
+
"@meith/theme-clubhouse": "0.17.2",
|
|
84
|
+
"@meith/theme-default": "0.17.2",
|
|
85
|
+
"@meith/theme-kit": "0.17.2",
|
|
86
|
+
"@meith/theme-midnight": "0.17.2",
|
|
87
|
+
"@meith/theme-phasebook": "0.17.2",
|
|
88
|
+
"@meith/theme-raidframe": "0.17.2",
|
|
89
|
+
"@meith/threads": "0.17.2",
|
|
90
|
+
"@meith/ui": "0.17.2",
|
|
91
|
+
"@meith/upgrade": "0.17.2"
|
|
91
92
|
},
|
|
92
93
|
"scripts": {
|
|
93
94
|
"dev": "next dev",
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
package/public/sw.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const FALLBACK_HREF = '/notifications'
|
|
2
|
+
|
|
3
|
+
function readPayload(event) {
|
|
4
|
+
if (!event.data) return null
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const payload = event.data.json()
|
|
8
|
+
if (payload === null || typeof payload !== 'object') return null
|
|
9
|
+
if (typeof payload.title !== 'string' || payload.title === '') return null
|
|
10
|
+
return payload
|
|
11
|
+
} catch (_error) {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fallbackUrl() {
|
|
17
|
+
return new URL(FALLBACK_HREF, self.location.origin).href
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function targetUrl(href) {
|
|
21
|
+
const wanted = typeof href === 'string' && href !== '' ? href : FALLBACK_HREF
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(wanted, self.location.origin)
|
|
25
|
+
return url.origin === self.location.origin ? url.href : fallbackUrl()
|
|
26
|
+
} catch (_error) {
|
|
27
|
+
return fallbackUrl()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
self.addEventListener('install', () => {
|
|
32
|
+
self.skipWaiting()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
self.addEventListener('activate', (event) => {
|
|
36
|
+
event.waitUntil(self.clients.claim())
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
self.addEventListener('push', (event) => {
|
|
40
|
+
const payload = readPayload(event)
|
|
41
|
+
if (payload === null) return
|
|
42
|
+
|
|
43
|
+
const options = {
|
|
44
|
+
body: typeof payload.body === 'string' ? payload.body : '',
|
|
45
|
+
tag: typeof payload.id === 'number' ? `meith-${payload.id}` : 'meith',
|
|
46
|
+
data: { href: targetUrl(payload.href) },
|
|
47
|
+
icon: '/apple-icon.png',
|
|
48
|
+
badge: '/icon-light-32x32.png',
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof payload.badge === 'number' && 'setAppBadge' in navigator) {
|
|
53
|
+
navigator.setAppBadge(payload.badge).catch(() => {})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
event.waitUntil(self.registration.showNotification(payload.title, options))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
self.addEventListener('notificationclick', (event) => {
|
|
60
|
+
event.notification.close()
|
|
61
|
+
|
|
62
|
+
const href = targetUrl(event.notification.data && event.notification.data.href)
|
|
63
|
+
|
|
64
|
+
event.waitUntil(
|
|
65
|
+
self.clients
|
|
66
|
+
.matchAll({ type: 'window', includeUncontrolled: true })
|
|
67
|
+
.then((clients) => {
|
|
68
|
+
for (const client of clients) {
|
|
69
|
+
if (client.url === href && 'focus' in client) return client.focus()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const open = clients[0]
|
|
73
|
+
if (open !== undefined && 'navigate' in open) {
|
|
74
|
+
return open.focus().then(() => open.navigate(href))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return self.clients.openWindow(href)
|
|
78
|
+
})
|
|
79
|
+
.catch(() => self.clients.openWindow(href)),
|
|
80
|
+
)
|
|
81
|
+
})
|
|
@@ -62,16 +62,16 @@ export async function saveAdminSettingsAction(
|
|
|
62
62
|
if (result.changed.length > 0) {
|
|
63
63
|
const tags = [CacheTags.settings(), ...result.invalidates]
|
|
64
64
|
await drivers().cache.invalidateTags(tags)
|
|
65
|
-
for (const tag of tags) await emitEvent('cache.invalidated', { tag }, {})
|
|
66
|
-
|
|
67
65
|
revalidatePath('/admin/settings')
|
|
68
66
|
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
await Promise.all([
|
|
68
|
+
...tags.map((tag) => emitEvent('cache.invalidated', { tag }, {})),
|
|
69
|
+
emitEvent('settings.saved', { keys: result.changed }, { adminId: admin.session.userId }),
|
|
70
|
+
recordAdminAction({
|
|
71
|
+
action: 'settings.changed',
|
|
72
|
+
detail: { keys: result.changed },
|
|
73
|
+
}),
|
|
74
|
+
])
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
return { notice: result.changed.length === 0 ? 'unchanged' : 'saved' }
|
|
@@ -6,7 +6,7 @@ import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
|
|
|
6
6
|
|
|
7
7
|
import { activeDefinitions } from './plugin-host'
|
|
8
8
|
|
|
9
|
-
export const CODE_VERSION = '0.17.
|
|
9
|
+
export const CODE_VERSION = '0.17.2'
|
|
10
10
|
|
|
11
11
|
export async function pendingUpgradeNotice(): Promise<string | null> {
|
|
12
12
|
if (env.DATA_SOURCE !== 'postgres') return null
|
|
@@ -179,26 +179,26 @@ export async function banMemberAction(_prev: FormState, form: FormData): Promise
|
|
|
179
179
|
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
180
180
|
})
|
|
181
181
|
|
|
182
|
-
await emitEvent(
|
|
183
|
-
'user.banned',
|
|
184
|
-
{ userId: id, expiresAt: expiresAt?.toISOString() ?? null },
|
|
185
|
-
{ moderatorId: context.session.userId, reason: trimmedText(form, 'reason') || null },
|
|
186
|
-
)
|
|
187
|
-
|
|
188
182
|
refreshMemberScreens()
|
|
189
|
-
await recordAdminAction({
|
|
190
|
-
action: 'user.banned',
|
|
191
|
-
detail: { userId: id, days: days === '' ? null : Number(days) },
|
|
192
|
-
})
|
|
193
183
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
184
|
+
const [, undo] = await Promise.all([
|
|
185
|
+
emitEvent(
|
|
186
|
+
'user.banned',
|
|
187
|
+
{ userId: id, expiresAt: expiresAt?.toISOString() ?? null },
|
|
188
|
+
{ moderatorId: context.session.userId, reason: trimmedText(form, 'reason') || null },
|
|
189
|
+
),
|
|
190
|
+
issueAdminUndo({
|
|
197
191
|
actorUserId: context.session.userId,
|
|
198
192
|
operation: 'user.ban',
|
|
199
193
|
snapshot: { userId: id },
|
|
200
194
|
}),
|
|
201
|
-
|
|
195
|
+
recordAdminAction({
|
|
196
|
+
action: 'user.banned',
|
|
197
|
+
detail: { userId: id, days: days === '' ? null : Number(days) },
|
|
198
|
+
}),
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
return { notice: 'banned', undo }
|
|
202
202
|
} catch (err) {
|
|
203
203
|
return toFormState(err)
|
|
204
204
|
}
|
|
@@ -556,14 +556,16 @@ export async function liftBanAction(_prev: FormState, form: FormData): Promise<F
|
|
|
556
556
|
|
|
557
557
|
await banService().lift(id)
|
|
558
558
|
|
|
559
|
-
await emitEvent(
|
|
560
|
-
'user.unbanned',
|
|
561
|
-
{ userId: id, expired: false },
|
|
562
|
-
{ moderatorId: context.session.userId, reason: null },
|
|
563
|
-
)
|
|
564
|
-
|
|
565
559
|
refreshMemberScreens()
|
|
566
|
-
|
|
560
|
+
|
|
561
|
+
await Promise.all([
|
|
562
|
+
emitEvent(
|
|
563
|
+
'user.unbanned',
|
|
564
|
+
{ userId: id, expired: false },
|
|
565
|
+
{ moderatorId: context.session.userId, reason: null },
|
|
566
|
+
),
|
|
567
|
+
recordAdminAction({ action: 'user.ban_lifted', detail: { userId: id } }),
|
|
568
|
+
])
|
|
567
569
|
|
|
568
570
|
return { notice: 'lifted' }
|
|
569
571
|
} catch (err) {
|
package/src/server/user-admin.ts
CHANGED
|
@@ -118,13 +118,14 @@ export async function buildMemberView(userId: number): Promise<MemberView | null
|
|
|
118
118
|
|
|
119
119
|
const prefix = member.lastIpPrefix ?? member.registrationIpPrefix ?? ''
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
const [secondaryGroupIds, groups, activeBan, sharedNetwork] = await Promise.all([
|
|
122
|
+
repository.readSecondaryGroups(userId),
|
|
123
|
+
repository.listGroups(),
|
|
124
|
+
bans.findActive(userId),
|
|
125
|
+
repository.sharingIpPrefix(prefix, userId),
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
return { member, secondaryGroupIds, groups, activeBan, sharedNetwork }
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
export function requireUserBulk(): PostgresUserBulkRepository {
|