@meith/web 0.17.1 → 0.18.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/LICENSE.md +21 -165
- package/app/admin/api/marketplace/screenshot/route.ts +6 -3
- package/app/admin/forums/[id]/page.tsx +6 -3
- package/app/admin/groups/promotions/page.tsx +6 -0
- 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/app/api/system/tick/route.ts +27 -10
- package/bin/forum-web.mjs +292 -48
- package/bin/forum-web.test.ts +164 -1
- package/next.config.mjs +31 -64
- package/package.json +49 -48
- 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/group-admin.ts +2 -6
- package/src/server/install.ts +2 -1
- 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
package/next.config.mjs
CHANGED
|
@@ -4,36 +4,31 @@ 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
9
|
+
* where `forum-web` materializes at `.meith/app` and where this file sits in
|
|
10
|
+
* `apps/community` unmaterialized. `boards/stock` and `forum-web --at-root`
|
|
11
|
+
* are both at other depths, and neither relies on this default:
|
|
12
|
+
* `FORUM_WORKSPACE_ROOT` is set for the first by that board's own scripts
|
|
13
|
+
* and passed on by `forum-web` in every case, so this fallback only applies
|
|
14
|
+
* when the file is read outside that bin entirely (see
|
|
15
|
+
* docs/architecture.md, "The stock board"; docs/development.md, "Consuming
|
|
16
|
+
* the board from a workspace").
|
|
17
|
+
*/
|
|
24
18
|
const workspaceRoot = process.env.FORUM_WORKSPACE_ROOT
|
|
25
19
|
? path.resolve(process.env.FORUM_WORKSPACE_ROOT)
|
|
26
20
|
: path.join(here, '../../')
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The relative equivalent of `workspaceRoot`, for `outputFileTracingIncludes`
|
|
24
|
+
* below — see docs/development.md, "Consuming the board from a workspace",
|
|
25
|
+
* for why that option needs a path relative to this file rather than an
|
|
26
|
+
* absolute one. It is `.` rather than the empty string when this file already
|
|
27
|
+
* sits at the workspace root, which is what `forum-web build --at-root`
|
|
28
|
+
* materializes (same section) — an empty prefix would make the glob below
|
|
29
|
+
* read as an absolute path and match nothing.
|
|
30
|
+
*/
|
|
31
|
+
const upToWorkspaceRoot = path.relative(here, workspaceRoot).split(path.sep).join('/') || '.'
|
|
37
32
|
|
|
38
33
|
const loadedEnvFiles = []
|
|
39
34
|
for (const name of ['.env.local', '.env']) {
|
|
@@ -55,6 +50,7 @@ const nextConfig = {
|
|
|
55
50
|
serverExternalPackages: [
|
|
56
51
|
'@aws-sdk/client-s3',
|
|
57
52
|
'@aws-sdk/s3-request-presigner',
|
|
53
|
+
'@vercel/blob',
|
|
58
54
|
'postgres',
|
|
59
55
|
'@jsquash/jpeg',
|
|
60
56
|
'@jsquash/png',
|
|
@@ -62,49 +58,26 @@ const nextConfig = {
|
|
|
62
58
|
'nodemailer',
|
|
63
59
|
],
|
|
64
60
|
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.
|
|
61
|
+
/** See docs/development.md, "Consuming the board from a workspace", for why this is set. */
|
|
72
62
|
turbopack: {
|
|
73
63
|
root: workspaceRoot,
|
|
74
64
|
},
|
|
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.
|
|
65
|
+
/** Works around a gap in Next's own tracing — see docs/development.md, "Consuming the board from a workspace". */
|
|
82
66
|
outputFileTracingIncludes: {
|
|
83
67
|
'/**': [
|
|
84
68
|
`${upToWorkspaceRoot}/node_modules/.pnpm/@swc+helpers@0.5.23/node_modules/@swc/helpers/**/*`,
|
|
69
|
+
`${upToWorkspaceRoot}/node_modules/@swc/helpers/**/*`,
|
|
85
70
|
],
|
|
86
71
|
},
|
|
87
72
|
images: {
|
|
88
73
|
unoptimized: true,
|
|
89
74
|
},
|
|
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".
|
|
75
|
+
/**
|
|
76
|
+
* Every `@meith/*` package this app's dependency graph reaches, not just the
|
|
77
|
+
* ones apps/community imports directly — see docs/development.md,
|
|
78
|
+
* "Consuming the board from a workspace", and docs/release.md, "They ship
|
|
79
|
+
* TypeScript source, deliberately".
|
|
80
|
+
*/
|
|
108
81
|
transpilePackages: [
|
|
109
82
|
'@meith/accounts',
|
|
110
83
|
'@meith/admin',
|
|
@@ -152,12 +125,6 @@ const nextConfig = {
|
|
|
152
125
|
'@meith/threads',
|
|
153
126
|
'@meith/ui',
|
|
154
127
|
'@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
128
|
'@meith/web',
|
|
162
129
|
],
|
|
163
130
|
async headers() {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "The board itself: the Next.js app, and the forum-web bin that materializes it into an external board workspace.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/meith-dev/meith.git",
|
|
@@ -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/accounts": "0.
|
|
46
|
-
"@meith/
|
|
47
|
-
"@meith/
|
|
48
|
-
"@meith/
|
|
49
|
-
"@meith/attachments": "0.
|
|
50
|
-
"@meith/
|
|
51
|
-
"@meith/
|
|
52
|
-
"@meith/
|
|
53
|
-
"@meith/
|
|
54
|
-
"@meith/
|
|
55
|
-
"@meith/
|
|
56
|
-
"@meith/
|
|
57
|
-
"@meith/events": "0.
|
|
58
|
-
"@meith/
|
|
59
|
-
"@meith/
|
|
60
|
-
"@meith/
|
|
61
|
-
"@meith/
|
|
62
|
-
"@meith/mail": "0.
|
|
63
|
-
"@meith/
|
|
64
|
-
"@meith/
|
|
65
|
-
"@meith/
|
|
66
|
-
"@meith/
|
|
67
|
-
"@meith/
|
|
68
|
-
"@meith/
|
|
69
|
-
"@meith/
|
|
70
|
-
"@meith/plugin-kit": "0.
|
|
71
|
-
"@meith/
|
|
72
|
-
"@meith/
|
|
73
|
-
"@meith/
|
|
74
|
-
"@meith/
|
|
75
|
-
"@meith/
|
|
76
|
-
"@meith/
|
|
77
|
-
"@meith/
|
|
78
|
-
"@meith/settings": "0.
|
|
79
|
-
"@meith/
|
|
80
|
-
"@meith/
|
|
81
|
-
"@meith/tasks": "0.
|
|
82
|
-
"@meith/theme-clubhouse": "0.
|
|
83
|
-
"@meith/theme-kit": "0.
|
|
84
|
-
"@meith/theme-default": "0.
|
|
85
|
-
"@meith/theme-
|
|
86
|
-
"@meith/theme-
|
|
87
|
-
"@meith/
|
|
88
|
-
"@meith/
|
|
89
|
-
"@meith/
|
|
90
|
-
"@meith/upgrade": "0.
|
|
46
|
+
"@meith/accounts": "0.18.0",
|
|
47
|
+
"@meith/antispam": "0.18.0",
|
|
48
|
+
"@meith/api": "0.18.0",
|
|
49
|
+
"@meith/authorization": "0.18.0",
|
|
50
|
+
"@meith/attachments": "0.18.0",
|
|
51
|
+
"@meith/avatars": "0.18.0",
|
|
52
|
+
"@meith/admin": "0.18.0",
|
|
53
|
+
"@meith/db": "0.18.0",
|
|
54
|
+
"@meith/demo": "0.18.0",
|
|
55
|
+
"@meith/drafts": "0.18.0",
|
|
56
|
+
"@meith/drivers": "0.18.0",
|
|
57
|
+
"@meith/core": "0.18.0",
|
|
58
|
+
"@meith/events": "0.18.0",
|
|
59
|
+
"@meith/forums": "0.18.0",
|
|
60
|
+
"@meith/groups": "0.18.0",
|
|
61
|
+
"@meith/install": "0.18.0",
|
|
62
|
+
"@meith/i18n": "0.18.0",
|
|
63
|
+
"@meith/mail": "0.18.0",
|
|
64
|
+
"@meith/import": "0.18.0",
|
|
65
|
+
"@meith/markdown": "0.18.0",
|
|
66
|
+
"@meith/marketplace": "0.18.0",
|
|
67
|
+
"@meith/messages": "0.18.0",
|
|
68
|
+
"@meith/moderation": "0.18.0",
|
|
69
|
+
"@meith/plugin-dues": "0.18.0",
|
|
70
|
+
"@meith/notifications": "0.18.0",
|
|
71
|
+
"@meith/plugin-kit": "0.18.0",
|
|
72
|
+
"@meith/relations": "0.18.0",
|
|
73
|
+
"@meith/posts": "0.18.0",
|
|
74
|
+
"@meith/reputation": "0.18.0",
|
|
75
|
+
"@meith/polls": "0.18.0",
|
|
76
|
+
"@meith/runtime": "0.18.0",
|
|
77
|
+
"@meith/profile-fields": "0.18.0",
|
|
78
|
+
"@meith/search": "0.18.0",
|
|
79
|
+
"@meith/settings": "0.18.0",
|
|
80
|
+
"@meith/subscriptions": "0.18.0",
|
|
81
|
+
"@meith/signatures": "0.18.0",
|
|
82
|
+
"@meith/tasks": "0.18.0",
|
|
83
|
+
"@meith/theme-clubhouse": "0.18.0",
|
|
84
|
+
"@meith/theme-kit": "0.18.0",
|
|
85
|
+
"@meith/theme-default": "0.18.0",
|
|
86
|
+
"@meith/theme-phasebook": "0.18.0",
|
|
87
|
+
"@meith/theme-midnight": "0.18.0",
|
|
88
|
+
"@meith/ui": "0.18.0",
|
|
89
|
+
"@meith/threads": "0.18.0",
|
|
90
|
+
"@meith/theme-raidframe": "0.18.0",
|
|
91
|
+
"@meith/upgrade": "0.18.0"
|
|
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' }
|
|
@@ -77,14 +77,10 @@ export function requirePromotionRules(): PostgresPromotionRepository {
|
|
|
77
77
|
return repository
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export async function previewPromotions(
|
|
80
|
+
export async function previewPromotions(): Promise<PromotionRunResult | null> {
|
|
81
81
|
if (getContainer().dataSource !== 'postgres') return null
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
promotions: new PostgresPromotionRepository(getDb()),
|
|
85
|
-
guards: defaultPromotionGuards(),
|
|
86
|
-
})
|
|
87
|
-
return service.preview(limit)
|
|
83
|
+
return promotionService().preview()
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
export function promotionService(): PromotionService {
|
package/src/server/install.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getDb,
|
|
16
16
|
isInstalled,
|
|
17
17
|
markInstalled,
|
|
18
|
+
migrationUrl,
|
|
18
19
|
PostgresAdminRepository,
|
|
19
20
|
PostgresForumRepository,
|
|
20
21
|
PostgresSettingsRepository,
|
|
@@ -126,7 +127,7 @@ async function forgetCachedBoard(): Promise<void> {
|
|
|
126
127
|
export type InstallRun = { readonly sealed: true } | { readonly report: readonly StepOutcome[] }
|
|
127
128
|
|
|
128
129
|
export async function runInstall(input: InstallInput): Promise<InstallRun> {
|
|
129
|
-
const url = env
|
|
130
|
+
const url = migrationUrl(env)
|
|
130
131
|
|
|
131
132
|
try {
|
|
132
133
|
const run = await withInstallLock(url, async (): Promise<InstallRun> => {
|
|
@@ -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.
|
|
9
|
+
export const CODE_VERSION = '0.18.0'
|
|
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 {
|