refactor: remove vega project structure

pull/7976/head
NGPixel 7 months ago
parent fe38f4c7e5
commit b3c84fffbb
No known key found for this signature in database

@ -11,13 +11,13 @@ echo "Waiting for DB container to come online..."
/usr/local/bin/wait-for localhost:5432 -- echo "DB ready"
echo "Installing dependencies..."
cd server
pnpm install
cd ../ux
pnpm install
cd backend
npm install
cd ../frontend
npm install
cd ../blocks
pnpm install
pnpm build
npm install
npm build
cd ..
echo "Ready!"

12
.vscode/tasks.json vendored

@ -11,8 +11,8 @@
{
"label": "Create terminals",
"dependsOn": [
"Server",
"UX"
"Backend",
"Frontend"
],
// Mark as the default build task so cmd/ctrl+shift+b will create them
"group": {
@ -26,14 +26,14 @@
},
{
// The name that shows up in terminal tab
"label": "Server",
"label": "Backend",
// The task will launch a shell
"type": "shell",
"command": "/bin/zsh",
"args": [],
// Set the shell type
"options": {
"cwd": "/workspace/server"
"cwd": "/workspace/backend"
},
// Mark as a background task to avoid the spinner animation on the terminal tab
"isBackground": true,
@ -44,12 +44,12 @@
}
},
{
"label": "UX",
"label": "Frontend",
"type": "shell",
"command": "/bin/zsh",
"args": [],
"options": {
"cwd": "/workspace/ux",
"cwd": "/workspace/frontend",
},
"isBackground": true,
"problemMatcher": [],

File diff suppressed because it is too large Load Diff

@ -20,6 +20,5 @@
"glob": "11.0.0",
"rollup": "4.24.0",
"rollup-plugin-summary": "2.0.1"
},
"packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"
}
}

@ -1,900 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
lit:
specifier: 3.2.1
version: 3.2.1
devDependencies:
'@rollup/plugin-graphql':
specifier: 2.0.5
version: 2.0.5(graphql@16.8.1)(rollup@4.24.0)
'@rollup/plugin-node-resolve':
specifier: 15.3.0
version: 15.3.0(rollup@4.24.0)
'@rollup/plugin-terser':
specifier: 0.4.4
version: 0.4.4(rollup@4.24.0)
glob:
specifier: 11.0.0
version: 11.0.0
rollup:
specifier: 4.24.0
version: 4.24.0
rollup-plugin-summary:
specifier: 2.0.1
version: 2.0.1(rollup@4.24.0)
packages:
'@colors/colors@1.5.0':
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@jridgewell/gen-mapping@0.3.3':
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
engines: {node: '>=6.0.0'}
'@jridgewell/gen-mapping@0.3.5':
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
'@jridgewell/resolve-uri@3.1.1':
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
engines: {node: '>=6.0.0'}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.1.2':
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.2.1':
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.5':
resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
'@jridgewell/source-map@0.3.6':
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
'@jridgewell/sourcemap-codec@1.4.15':
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/trace-mapping@0.3.19':
resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@lit-labs/ssr-dom-shim@1.2.1':
resolution: {integrity: sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==}
'@lit/reactive-element@2.0.4':
resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==}
'@rollup/plugin-graphql@2.0.5':
resolution: {integrity: sha512-NMx9uVIheYMOQHV9Aann0sk/2+henT8T5JUbHneOvbD3iUrVUC7AaZ1B1ELJ/1vhqUe2OQIkRtGHzOQwBLoCvg==}
engines: {node: '>=14.0.0'}
peerDependencies:
graphql: '>=0.9.0'
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/plugin-node-resolve@15.3.0':
resolution: {integrity: sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.78.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/plugin-terser@0.4.4':
resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/pluginutils@5.1.2':
resolution: {integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.24.0':
resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.24.0':
resolution: {integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.24.0':
resolution: {integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.24.0':
resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-linux-arm-gnueabihf@4.24.0':
resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.24.0':
resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.24.0':
resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.24.0':
resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-powerpc64le-gnu@4.24.0':
resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.24.0':
resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.24.0':
resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.24.0':
resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.24.0':
resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==}
cpu: [x64]
os: [linux]
'@rollup/rollup-win32-arm64-msvc@4.24.0':
resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.24.0':
resolution: {integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.24.0':
resolution: {integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
acorn@8.10.0:
resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==}
engines: {node: '>=0.4.0'}
hasBin: true
acorn@8.13.0:
resolution: {integrity: sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==}
engines: {node: '>=0.4.0'}
hasBin: true
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.1.0:
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
brotli-size@4.0.0:
resolution: {integrity: sha512-uA9fOtlTRC0iqKfzff1W34DXUA3GyVqbUaeo3Rw3d4gd1eavKVCETXrn3NzO74W+UVkG3UHu8WxUi+XvKI/huA==}
engines: {node: '>= 10.16.0'}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
cli-table3@0.6.5:
resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
engines: {node: 10.* || >= 12.*}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
duplexer@0.1.1:
resolution: {integrity: sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
filesize@10.1.6:
resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==}
engines: {node: '>= 10.4.0'}
foreground-child@3.3.0:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
glob@11.0.0:
resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
engines: {node: 20 || >=22}
hasBin: true
graphql-tag@2.12.6:
resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==}
engines: {node: '>=10'}
peerDependencies:
graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
graphql@16.8.1:
resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
gzip-size@7.0.0:
resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
is-core-module@2.15.1:
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
engines: {node: '>= 0.4'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-module@1.0.0:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jackspeak@4.0.2:
resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==}
engines: {node: 20 || >=22}
lit-element@4.1.1:
resolution: {integrity: sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==}
lit-html@3.2.1:
resolution: {integrity: sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==}
lit@3.2.1:
resolution: {integrity: sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==}
lru-cache@11.0.1:
resolution: {integrity: sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==}
engines: {node: 20 || >=22}
minimatch@10.0.1:
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
engines: {node: 20 || >=22}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
path-scurry@2.0.0:
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
engines: {node: 20 || >=22}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
rollup-plugin-summary@2.0.1:
resolution: {integrity: sha512-aO8t5ZxAB9svdUl4etKUbWr7FFK/2RLj81ZiHnsJKsXPcZEekh32+TknJFj/V8JYRqjmlb/Ygi4aktRa9LdQjw==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.68.0||^3.0.0||^4.0.0
rollup@4.24.0:
resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
serialize-javascript@6.0.1:
resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
smob@1.4.1:
resolution: {integrity: sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
terser@5.21.0:
resolution: {integrity: sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==}
engines: {node: '>=10'}
hasBin: true
terser@5.36.0:
resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==}
engines: {node: '>=10'}
hasBin: true
tslib@2.8.0:
resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
snapshots:
'@colors/colors@1.5.0':
optional: true
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.0
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@jridgewell/gen-mapping@0.3.3':
dependencies:
'@jridgewell/set-array': 1.1.2
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.19
'@jridgewell/gen-mapping@0.3.5':
dependencies:
'@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.1': {}
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.1.2': {}
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/source-map@0.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.19
'@jridgewell/source-map@0.3.6':
dependencies:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/sourcemap-codec@1.4.15': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/trace-mapping@0.3.19':
dependencies:
'@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@lit-labs/ssr-dom-shim@1.2.1': {}
'@lit/reactive-element@2.0.4':
dependencies:
'@lit-labs/ssr-dom-shim': 1.2.1
'@rollup/plugin-graphql@2.0.5(graphql@16.8.1)(rollup@4.24.0)':
dependencies:
'@rollup/pluginutils': 5.1.2(rollup@4.24.0)
graphql: 16.8.1
graphql-tag: 2.12.6(graphql@16.8.1)
optionalDependencies:
rollup: 4.24.0
'@rollup/plugin-node-resolve@15.3.0(rollup@4.24.0)':
dependencies:
'@rollup/pluginutils': 5.1.2(rollup@4.24.0)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.8
optionalDependencies:
rollup: 4.24.0
'@rollup/plugin-terser@0.4.4(rollup@4.24.0)':
dependencies:
serialize-javascript: 6.0.1
smob: 1.4.1
terser: 5.21.0
optionalDependencies:
rollup: 4.24.0
'@rollup/pluginutils@5.1.2(rollup@4.24.0)':
dependencies:
'@types/estree': 1.0.6
estree-walker: 2.0.2
picomatch: 2.3.1
optionalDependencies:
rollup: 4.24.0
'@rollup/rollup-android-arm-eabi@4.24.0':
optional: true
'@rollup/rollup-android-arm64@4.24.0':
optional: true
'@rollup/rollup-darwin-arm64@4.24.0':
optional: true
'@rollup/rollup-darwin-x64@4.24.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.24.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.24.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.24.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.24.0':
optional: true
'@rollup/rollup-linux-powerpc64le-gnu@4.24.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.24.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.24.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.24.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.24.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.24.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.24.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.24.0':
optional: true
'@types/estree@1.0.6': {}
'@types/resolve@1.20.2': {}
'@types/trusted-types@2.0.7': {}
acorn@8.10.0: {}
acorn@8.13.0: {}
ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
balanced-match@1.0.2: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
brotli-size@4.0.0:
dependencies:
duplexer: 0.1.1
buffer-from@1.1.2: {}
cli-table3@0.6.5:
dependencies:
string-width: 4.2.3
optionalDependencies:
'@colors/colors': 1.5.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
commander@2.20.3: {}
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
deepmerge@4.3.1: {}
duplexer@0.1.1: {}
duplexer@0.1.2: {}
eastasianwidth@0.2.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
estree-walker@2.0.2: {}
filesize@10.1.6: {}
foreground-child@3.3.0:
dependencies:
cross-spawn: 7.0.3
signal-exit: 4.1.0
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
glob@11.0.0:
dependencies:
foreground-child: 3.3.0
jackspeak: 4.0.2
minimatch: 10.0.1
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.0
graphql-tag@2.12.6(graphql@16.8.1):
dependencies:
graphql: 16.8.1
tslib: 2.8.0
graphql@16.8.1: {}
gzip-size@7.0.0:
dependencies:
duplexer: 0.1.2
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
is-core-module@2.15.1:
dependencies:
hasown: 2.0.2
is-fullwidth-code-point@3.0.0: {}
is-module@1.0.0: {}
isexe@2.0.0: {}
jackspeak@4.0.2:
dependencies:
'@isaacs/cliui': 8.0.2
lit-element@4.1.1:
dependencies:
'@lit-labs/ssr-dom-shim': 1.2.1
'@lit/reactive-element': 2.0.4
lit-html: 3.2.1
lit-html@3.2.1:
dependencies:
'@types/trusted-types': 2.0.7
lit@3.2.1:
dependencies:
'@lit/reactive-element': 2.0.4
lit-element: 4.1.1
lit-html: 3.2.1
lru-cache@11.0.1: {}
minimatch@10.0.1:
dependencies:
brace-expansion: 2.0.1
minipass@7.1.2: {}
package-json-from-dist@1.0.1: {}
path-key@3.1.1: {}
path-parse@1.0.7: {}
path-scurry@2.0.0:
dependencies:
lru-cache: 11.0.1
minipass: 7.1.2
picomatch@2.3.1: {}
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1
resolve@1.22.8:
dependencies:
is-core-module: 2.15.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
rollup-plugin-summary@2.0.1(rollup@4.24.0):
dependencies:
brotli-size: 4.0.0
cli-table3: 0.6.5
filesize: 10.1.6
gzip-size: 7.0.0
rollup: 4.24.0
terser: 5.36.0
rollup@4.24.0:
dependencies:
'@types/estree': 1.0.6
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.24.0
'@rollup/rollup-android-arm64': 4.24.0
'@rollup/rollup-darwin-arm64': 4.24.0
'@rollup/rollup-darwin-x64': 4.24.0
'@rollup/rollup-linux-arm-gnueabihf': 4.24.0
'@rollup/rollup-linux-arm-musleabihf': 4.24.0
'@rollup/rollup-linux-arm64-gnu': 4.24.0
'@rollup/rollup-linux-arm64-musl': 4.24.0
'@rollup/rollup-linux-powerpc64le-gnu': 4.24.0
'@rollup/rollup-linux-riscv64-gnu': 4.24.0
'@rollup/rollup-linux-s390x-gnu': 4.24.0
'@rollup/rollup-linux-x64-gnu': 4.24.0
'@rollup/rollup-linux-x64-musl': 4.24.0
'@rollup/rollup-win32-arm64-msvc': 4.24.0
'@rollup/rollup-win32-ia32-msvc': 4.24.0
'@rollup/rollup-win32-x64-msvc': 4.24.0
fsevents: 2.3.3
safe-buffer@5.2.1: {}
serialize-javascript@6.0.1:
dependencies:
randombytes: 2.1.0
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
smob@1.4.1: {}
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
source-map@0.6.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.0:
dependencies:
ansi-regex: 6.1.0
supports-preserve-symlinks-flag@1.0.0: {}
terser@5.21.0:
dependencies:
'@jridgewell/source-map': 0.3.5
acorn: 8.10.0
commander: 2.20.3
source-map-support: 0.5.21
terser@5.36.0:
dependencies:
'@jridgewell/source-map': 0.3.6
acorn: 8.13.0
commander: 2.20.3
source-map-support: 0.5.21
tslib@2.8.0: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.1
string-width: 5.1.2
strip-ansi: 7.1.0

@ -123,7 +123,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@ -1067,6 +1066,7 @@
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
@ -1077,6 +1077,7 @@
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
@ -1091,7 +1092,8 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
"version": "1.1.12",
@ -1099,6 +1101,7 @@
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1110,6 +1113,7 @@
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -1123,6 +1127,7 @@
"integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
@ -1133,6 +1138,7 @@
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@types/json-schema": "^7.0.15"
},
@ -1146,6 +1152,7 @@
"integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ajv": "^6.14.0",
"debug": "^4.3.2",
@ -1169,7 +1176,8 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.12",
@ -1177,6 +1185,7 @@
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1188,6 +1197,7 @@
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -1201,6 +1211,7 @@
"integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@ -1214,6 +1225,7 @@
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
@ -1224,6 +1236,7 @@
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@eslint/core": "^0.15.2",
"levn": "^0.4.1"
@ -1238,6 +1251,7 @@
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=18.18.0"
}
@ -1248,6 +1262,7 @@
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@humanfs/core": "^0.19.1",
"@humanwhocodes/retry": "^0.4.0"
@ -1262,6 +1277,7 @@
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=12.22"
},
@ -1276,6 +1292,7 @@
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=18.18"
},
@ -2846,13 +2863,15 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/lodash": {
"version": "4.17.24",
@ -2876,7 +2895,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/mime": {
"version": "1.3.5",
@ -3101,7 +3121,6 @@
"integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rolldown/pluginutils": "1.0.0-rc.2"
},
@ -3432,7 +3451,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -3456,6 +3474,7 @@
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -3937,7 +3956,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -4055,6 +4073,7 @@
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
@ -4403,7 +4422,8 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/confbox": {
"version": "0.1.8",
@ -4560,7 +4580,8 @@
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/default-browser": {
"version": "5.5.0",
@ -4962,6 +4983,7 @@
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@ -4997,6 +5019,7 @@
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@ -5058,6 +5081,7 @@
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@ -5087,7 +5111,8 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.12",
@ -5095,6 +5120,7 @@
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -5106,6 +5132,7 @@
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@ -5119,6 +5146,7 @@
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@ -5132,6 +5160,7 @@
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -5145,6 +5174,7 @@
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
@ -5163,6 +5193,7 @@
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@ -5190,6 +5221,7 @@
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"estraverse": "^5.1.0"
},
@ -5203,6 +5235,7 @@
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"estraverse": "^5.2.0"
},
@ -5351,7 +5384,8 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fast-fifo": {
"version": "1.3.2",
@ -5382,14 +5416,16 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fastq": {
"version": "1.20.1",
@ -5424,6 +5460,7 @@
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"flat-cache": "^4.0.0"
},
@ -5501,6 +5538,7 @@
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
@ -5528,6 +5566,7 @@
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"flatted": "^3.2.9",
"keyv": "^4.5.4"
@ -5541,7 +5580,8 @@
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"license": "ISC"
"license": "ISC",
"peer": true
},
"node_modules/foreground-child": {
"version": "3.3.1",
@ -5773,6 +5813,7 @@
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@ -5967,6 +6008,7 @@
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 4"
}
@ -5984,6 +6026,7 @@
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@ -6001,6 +6044,7 @@
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.8.19"
}
@ -6349,6 +6393,7 @@
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^2.0.1"
},
@ -6373,21 +6418,24 @@
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/json5": {
"version": "2.2.3",
@ -6501,6 +6549,7 @@
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"json-buffer": "3.0.1"
}
@ -6593,6 +6642,7 @@
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@ -6650,6 +6700,7 @@
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"p-locate": "^5.0.0"
},
@ -6678,7 +6729,8 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.repeat": {
"version": "4.1.0",
@ -6773,7 +6825,6 @@
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
@ -7164,7 +7215,8 @@
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/negotiator": {
"version": "0.6.4",
@ -7330,6 +7382,7 @@
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@ -7378,6 +7431,7 @@
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
@ -7394,6 +7448,7 @@
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"p-limit": "^3.0.2"
},
@ -7434,6 +7489,7 @@
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"callsites": "^3.0.0"
},
@ -7468,6 +7524,7 @@
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@ -7547,7 +7604,6 @@
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/devtools-api": "^7.7.7"
},
@ -7633,7 +7689,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -7656,6 +7711,7 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8.0"
}
@ -7914,6 +7970,7 @@
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
@ -7964,7 +8021,6 @@
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.18.6.tgz",
"integrity": "sha512-ZlK+vJXOBPSFDCNQDBDNwSI+AHoqaFPxK8ve6mhsYLhMKWI5b8zsGY9VU1xYjngO2aBvU4fvGWXy4tTbzrBk8Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 10.18.1",
"npm": ">= 6.13.4",
@ -8192,6 +8248,7 @@
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
}
@ -8240,7 +8297,6 @@
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@ -9351,6 +9407,7 @@
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
},
@ -9645,6 +9702,7 @@
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1"
},
@ -9684,7 +9742,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -9801,6 +9858,7 @@
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"punycode": "^2.1.0"
}
@ -9878,7 +9936,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@ -10557,7 +10614,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.29",
"@vue/compiler-sfc": "3.5.29",
@ -10579,7 +10635,6 @@
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz",
"integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@intlify/core-base": "11.2.8",
"@intlify/shared": "11.2.8",
@ -10802,6 +10857,7 @@
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -10969,6 +11025,7 @@
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},

@ -1,5 +0,0 @@
audit = false
fund = false
lockfile-version = "3"
save-exact = true
save-prefix = ""

@ -1,4 +0,0 @@
<!-- TITLE: {TITLE} -->
<!-- SUBTITLE: A quick summary of {TITLE} -->
# Header

@ -1,159 +0,0 @@
# ---------------------------------
# DO NOT EDIT THIS FILE!
# This is reserved for system use!
# ---------------------------------
name: Wiki.js
defaults:
config:
# File defaults
port: 80
db:
host: localhost
port: 5432
user: wikijs
pass: wikijsrocks
db: wiki
ssl: false
sslOptions:
auto: true
schema: wiki
ssl:
enabled: false
pool:
min: 1
bindIP: 0.0.0.0
logLevel: info
logFormat: default
offline: false
dataPath: ./data
bodyParserLimit: 5mb
scheduler:
workers: 3
pollingCheck: 5
scheduledCheck: 300
maxRetries: 2
retryBackoff: 60
historyExpiration: 90000
# DB defaults
api:
isEnabled: false
mail:
host: ''
secure: true
verifySSL: true
metrics:
isEnabled: false
auth:
autoLogin: false
enforce2FA: false
hideLocal: false
loginBgUrl: ''
audience: 'urn:wiki.js'
tokenExpiration: '30m'
tokenRenewal: '14d'
security:
corsMode: 'OFF'
corsConfig: ''
enforceCsp: false
trustProxy: false
enforceHsts: false
disallowFloc: true
hstsDuration: 0
cspDirectives: ''
uploadScanSVG: true
disallowIframe: true
uploadMaxFiles: 20
authJwtAudience: 'urn:wiki.js'
authJwtExpiration: '30m'
uploadMaxFileSize: 10485760
forceAssetDownload: true
disallowOpenRedirect: true
authJwtRenewablePeriod: '14d'
enforceSameOriginReferrerPolicy: true
flags:
experimental: false
authDebug: false
sqlLog: false
userDefaults:
timezone: 'America/New_York'
dateFormat: 'YYYY-MM-DD'
timeFormat: '12h'
# System defaults
channel: NEXT
cors:
credentials: true
maxAge: 600
methods: 'GET,POST'
origin: true
maintainerEmail: security@requarks.io
tsDictMappings:
ar: arabic
hy: armenian
eu: basque
ca: catalan
da: danish
nl: dutch
en: english
fi: finnish
fr: french
de: german
el: greek
hi: hindi
hu: hungarian
id: indonesian
ga: irish
it: italian
lt: lithuanian
ne: nepali
no: norwegian
pt: portuguese
ro: romanian
ru: russian
sr: serbian
es: spanish
sv: swedish
ta: tamil
tr: turkish
yi: yiddish
editors:
asciidoc:
contentType: html
config: {}
markdown:
contentType: markdown
config:
allowHTML: true
linkify: true
lineBreaks: true
typographer: false
underline: false
tabWidth: 2
latexEngine: katex
kroki: true
plantuml: true
multimdTable: true
wysiwyg:
contentType: html
config: {}
systemIds:
localAuthId: '5a528c4c-0a82-4ad2-96a5-2b23811e6588'
guestsGroupId: '10000000-0000-4000-8000-000000000001'
usersGroupId: '20000000-0000-4000-8000-000000000002'
groups:
defaultPermissions:
- 'read:pages'
- 'read:assets'
- 'read:comments'
- 'write:comments'
defaultRules:
- name: Default Rule
mode: ALLOW
match: START
roles:
- 'read:pages'
- 'read:assets'
- 'read:comments'
- 'write:comments'
path: ''
locales: []
sites: []

@ -1,7 +0,0 @@
export default {
arabic: '\u0600-\u06ff\u0750-\u077f\ufb50-\ufc3f\ufe70-\ufefc',
cjk: '\u4E00-\u9FBF\u3040-\u309F\u30A0-\u30FFㄱ-ㅎ가-힣ㅏ-ㅣ',
youtube: /(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/,
vimeo: /vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/,
dailymotion: /(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/
}

@ -1,159 +0,0 @@
import express from 'express'
import ExpressBrute from 'express-brute'
import BruteKnex from '../helpers/brute-knex.mjs'
import { find, isEmpty, set } from 'lodash-es'
import path from 'node:path'
import { DateTime } from 'luxon'
export default function () {
const router = express.Router()
const bruteforce = new ExpressBrute(new BruteKnex({
createTable: true,
knex: WIKI.db.knex
}), {
freeRetries: 5,
minWait: 5 * 60 * 1000, // 5 minutes
maxWait: 60 * 60 * 1000, // 1 hour
failCallback: (req, res, next) => {
res.status(401).send('Too many failed attempts. Try again later.')
}
})
/**
* Login form
*/
router.get('/login', async (req, res, next) => {
// -> Bypass Login
if (WIKI.config.auth.autoLogin && !req.query.all) {
const stg = await WIKI.db.authentication.query().orderBy('order').first()
const stgInfo = find(WIKI.data.authentication, ['key', stg.strategyKey])
if (!stgInfo.useForm) {
return res.redirect(`/login/${stg.key}`)
}
}
// -> Show Login
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
})
/**
* Social Strategies Login
*/
router.get('/login/:strategy', async (req, res, next) => {
try {
await WIKI.db.users.login({
strategy: req.params.strategy
}, { req, res })
} catch (err) {
next(err)
}
})
/**
* Social Strategies Callback
*/
router.all('/login/:strategy/callback', async (req, res, next) => {
if (req.method !== 'GET' && req.method !== 'POST') { return next() }
try {
const authResult = await WIKI.db.users.login({
strategy: req.params.strategy
}, { req, res })
res.cookie('jwt', authResult.jwt, { expires: DateTime.now().plus({ years: 1 }).toJSDate() })
const loginRedirect = req.cookies['loginRedirect']
if (loginRedirect === '/' && authResult.redirect) {
res.clearCookie('loginRedirect')
res.redirect(authResult.redirect)
} else if (loginRedirect) {
res.clearCookie('loginRedirect')
res.redirect(loginRedirect)
} else if (authResult.redirect) {
res.redirect(authResult.redirect)
} else {
res.redirect('/')
}
} catch (err) {
next(err)
}
})
/**
* Logout
*/
router.get('/logout', async (req, res, next) => {
const redirURL = await WIKI.db.users.logout({ req, res })
req.logout((err) => {
if (err) { return next(err) }
res.clearCookie('jwt')
res.redirect(redirURL)
})
})
/**
* Register form
*/
router.get('/register', async (req, res, next) => {
set(res.locals, 'pageMeta.title', 'Register')
const localStrg = await WIKI.db.authentication.getStrategy('local')
if (localStrg.selfRegistration) {
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
} else {
next(new WIKI.Error.AuthRegistrationDisabled())
}
})
/**
* Verify
*/
router.get('/verify/:token', bruteforce.prevent, async (req, res, next) => {
try {
const usr = await WIKI.db.userKeys.validateToken({ kind: 'verify', token: req.params.token })
await WIKI.db.users.query().patch({ isVerified: true }).where('id', usr.id)
req.brute.reset()
if (WIKI.config.auth.enforce2FA) {
res.redirect('/login')
} else {
const result = await WIKI.db.users.refreshToken(usr)
res.cookie('jwt', result.token, { expires: DateTime.now().plus({ years: 1 }).toJSDate() })
res.redirect('/')
}
} catch (err) {
next(err)
}
})
/**
* Reset Password
*/
router.get('/login-reset/:token', bruteforce.prevent, async (req, res, next) => {
try {
const usr = await WIKI.db.userKeys.validateToken({ kind: 'resetPwd', token: req.params.token })
if (!usr) {
throw new Error('Invalid Token')
}
req.brute.reset()
const changePwdContinuationToken = await WIKI.db.userKeys.generateToken({
userId: usr.id,
kind: 'changePwd'
})
const bgUrl = !isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg'
res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal, changePwdContinuationToken })
} catch (err) {
next(err)
}
})
/**
* JWT Public Endpoints
*/
router.get('/.well-known/jwk.json', function (req, res, next) {
res.json(WIKI.config.certs.jwk)
})
router.get('/.well-known/jwk.pem', function (req, res, next) {
res.send(WIKI.config.certs.public)
})
return router
}

@ -1,562 +0,0 @@
import express from 'express'
import { parsePath } from '../helpers/page.mjs'
// import CleanCSS from 'clean-css'
import path from 'node:path'
export default function () {
const router = express.Router()
const siteAssetsPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets')
/**
* Robots.txt
*/
router.get('/robots.txt', (req, res, next) => {
res.type('text/plain')
if (WIKI.config.seo.robots.includes('noindex')) {
res.send('User-agent: *\nDisallow: /')
} else {
res.status(200).end()
}
})
/**
* Health Endpoint
*/
router.get('/healthz', (req, res, next) => {
if (WIKI.db.knex.client.pool.numFree() < 1 && WIKI.db.knex.client.pool.numUsed() < 1) {
res.status(503).json({ ok: false }).end()
} else {
res.status(200).json({ ok: true }).end()
}
})
/**
* Site Asset
*/
router.get('/_site{/:siteId}/:resource', async (req, res, next) => {
const site = req.params.siteId ? WIKI.sites[req.params.siteId] : await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
if (!site) {
return res.status(404).send('Site Not Found')
}
switch (req.params.resource) {
case 'logo': {
if (site.config.assets.logo) {
// TODO: Fetch from db if not in disk cache
res.sendFile(path.join(siteAssetsPath, `logo-${site.id}.${site.config.assets.logoExt}`))
} else {
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
}
break
}
case 'favicon': {
if (site.config.assets.favicon) {
// TODO: Fetch from db if not in disk cache
res.sendFile(path.join(siteAssetsPath, `favicon-${site.id}.${site.config.assets.faviconExt}`))
} else {
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
}
break
}
case 'loginbg': {
if (site.config.assets.loginBg) {
// TODO: Fetch from db if not in disk cache
res.sendFile(path.join(siteAssetsPath, `loginbg-${site.id}.jpg`))
} else {
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/bg/login.jpg'))
}
break
}
default: {
return res.status(404).send('Invalid Site Resource')
}
}
})
/**
* Asset Thumbnails / Download
*/
router.get('/_thumb/:id.webp', async (req, res, next) => {
const thumb = await WIKI.db.assets.getThumbnail({
id: req.params.id
})
if (thumb) {
// TODO: Check permissions
switch (thumb.previewState) {
case 'pending': {
res.redirect('/_assets/illustrations/fileman-pending.svg')
break
}
case 'ready': {
res.set('Content-Type', 'image/webp')
res.send(thumb.preview)
break
}
case 'failed': {
res.redirect('/_assets/illustrations/fileman-failed.svg')
break
}
default: {
return res.status(500).send('Invalid Thumbnail Preview State')
}
}
} else {
return res.sendStatus(404)
}
})
// router.get(['/_admin', '/_admin/*'], (req, res, next) => {
// if (!WIKI.auth.checkAccess(req.user, [
// 'manage:system',
// 'write:users',
// 'manage:users',
// 'write:groups',
// 'manage:groups',
// 'manage:navigation',
// 'manage:theme',
// 'manage:api'
// ])) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.status(403).render('unauthorized', { action: 'view' })
// }
// _.set(res.locals, 'pageMeta.title', 'Admin')
// res.render('admin')
// })
// /**
// * Download Page / Version
// */
// router.get(['/d', '/d/*'], async (req, res, next) => {
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
// const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
// const page = await WIKI.db.pages.getPageFromDb({
// path: pageArgs.path,
// locale: pageArgs.locale,
// userId: req.user.id,
// isPrivate: false
// })
// pageArgs.tags = _.get(page, 'tags', [])
// if (versionId > 0) {
// if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.render('unauthorized', { action: 'downloadVersion' })
// }
// } else {
// if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.render('unauthorized', { action: 'download' })
// }
// }
// if (page) {
// const fileName = _.last(page.path.split('/')) + '.' + pageHelper.getFileExtension(page.contentType)
// res.attachment(fileName)
// if (versionId > 0) {
// const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
// res.send(pageHelper.injectPageMetadata(pageVersion))
// } else {
// res.send(pageHelper.injectPageMetadata(page))
// }
// } else {
// res.status(404).end()
// }
// })
// /**
// * Create/Edit document
// */
// router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
// const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
// if (!site) {
// throw new Error('INVALID_SITE')
// }
// if (pageArgs.path === '') {
// return res.redirect(`/_edit/home`)
// }
// // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
// // return res.redirect(`/_edit/${pageArgs.locale}/${pageArgs.path}`)
// // }
// // req.i18n.changeLanguage(pageArgs.locale)
// // -> Set Editor Lang
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
// // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
// // -> Check for reserved path
// if (pageHelper.isReservedPath(pageArgs.path)) {
// return next(new Error('Cannot create this page because it starts with a system reserved path.'))
// }
// // -> Get page data from DB
// let page = await WIKI.db.pages.getPageFromDb({
// siteId: site.id,
// path: pageArgs.path,
// locale: pageArgs.locale,
// userId: req.user.id
// })
// pageArgs.tags = _.get(page, 'tags', [])
// // -> Effective Permissions
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
// const injectCode = {
// css: '', // WIKI.config.theming.injectCSS,
// head: '', // WIKI.config.theming.injectHead,
// body: '' // WIKI.config.theming.injectBody
// }
// if (page) {
// // -> EDIT MODE
// if (!(effectivePermissions.pages.write || effectivePermissions.pages.manage)) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.render('unauthorized', { action: 'edit' })
// }
// // -> Get page tags
// await page.$relatedQuery('tags')
// page.tags = _.map(page.tags, 'tag')
// // Handle missing extra field
// page.extra = page.extra || { css: '', js: '' }
// // -> Beautify Script CSS
// if (!_.isEmpty(page.extra.css)) {
// page.extra.css = new CleanCSS({ format: 'beautify' }).minify(page.extra.css).styles
// }
// _.set(res.locals, 'pageMeta.title', `Edit ${page.title}`)
// _.set(res.locals, 'pageMeta.description', page.description)
// page.mode = 'update'
// page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false'
// page.content = Buffer.from(page.content).toString('base64')
// } else {
// // -> CREATE MODE
// if (!effectivePermissions.pages.write) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.render('unauthorized', { action: 'create' })
// }
// _.set(res.locals, 'pageMeta.title', `New Page`)
// page = {
// path: pageArgs.path,
// localeCode: pageArgs.locale,
// editorKey: null,
// mode: 'create',
// content: null,
// title: null,
// description: null,
// updatedAt: new Date().toISOString(),
// extra: {
// css: '',
// js: ''
// }
// }
// }
// res.render('editor', { page, injectCode, effectivePermissions })
// })
// /**
// * History
// */
// router.get(['/h', '/h/*'], async (req, res, next) => {
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
// if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
// return res.redirect(`/h/${pageArgs.locale}/${pageArgs.path}`)
// }
// req.i18n.changeLanguage(pageArgs.locale)
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
// _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
// const page = await WIKI.db.pages.getPageFromDb({
// path: pageArgs.path,
// locale: pageArgs.locale,
// userId: req.user.id,
// isPrivate: false
// })
// if (!page) {
// _.set(res.locals, 'pageMeta.title', 'Page Not Found')
// return res.status(404).render('notfound', { action: 'history' })
// }
// pageArgs.tags = _.get(page, 'tags', [])
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
// if (!effectivePermissions.history.read) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.render('unauthorized', { action: 'history' })
// }
// if (page) {
// _.set(res.locals, 'pageMeta.title', page.title)
// _.set(res.locals, 'pageMeta.description', page.description)
// res.render('history', { page, effectivePermissions })
// } else {
// res.redirect(`/${pageArgs.path}`)
// }
// })
// /**
// * Page ID redirection
// */
// router.get(['/i', '/i/:id'], async (req, res, next) => {
// const pageId = _.toSafeInteger(req.params.id)
// if (pageId <= 0) {
// return res.redirect('/')
// }
// const page = await WIKI.db.pages.query().column(['path', 'localeCode', 'isPrivate', 'privateNS']).findById(pageId)
// if (!page) {
// _.set(res.locals, 'pageMeta.title', 'Page Not Found')
// return res.status(404).render('notfound', { action: 'view' })
// }
// if (!WIKI.auth.checkAccess(req.user, ['read:pages'], {
// locale: page.localeCode,
// path: page.path,
// private: page.isPrivate,
// privateNS: page.privateNS,
// explicitLocale: false,
// tags: page.tags
// })) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.render('unauthorized', { action: 'view' })
// }
// if (WIKI.config.lang.namespacing) {
// return res.redirect(`/${page.localeCode}/${page.path}`)
// } else {
// return res.redirect(`/${page.path}`)
// }
// })
// /**
// * Source
// */
// router.get(['/s', '/s/*'], async (req, res, next) => {
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
// const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
// const page = await WIKI.db.pages.getPageFromDb({
// path: pageArgs.path,
// locale: pageArgs.locale,
// userId: req.user.id,
// isPrivate: false
// })
// pageArgs.tags = _.get(page, 'tags', [])
// if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
// return res.redirect(`/s/${pageArgs.locale}/${pageArgs.path}`)
// }
// // -> Effective Permissions
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
// _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
// if (versionId > 0) {
// if (!effectivePermissions.history.read) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.render('unauthorized', { action: 'sourceVersion' })
// }
// } else {
// if (!effectivePermissions.source.read) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.render('unauthorized', { action: 'source' })
// }
// }
// if (page) {
// if (versionId > 0) {
// const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
// _.set(res.locals, 'pageMeta.title', pageVersion.title)
// _.set(res.locals, 'pageMeta.description', pageVersion.description)
// res.render('source', {
// page: {
// ...page,
// ...pageVersion
// },
// effectivePermissions
// })
// } else {
// _.set(res.locals, 'pageMeta.title', page.title)
// _.set(res.locals, 'pageMeta.description', page.description)
// res.render('source', { page, effectivePermissions })
// }
// } else {
// res.redirect(`/${pageArgs.path}`)
// }
// })
// /**
// * Tags
// */
// router.get(['/t', '/t/*'], (req, res, next) => {
// _.set(res.locals, 'pageMeta.title', 'Tags')
// res.render('tags')
// })
/**
* User Avatar
*/
router.get('/_user/:uid/avatar', async (req, res, next) => {
if (!WIKI.auth.checkAccess(req.user, ['read:pages'])) {
return res.sendStatus(403)
}
const av = await WIKI.db.users.getUserAvatarData(req.params.uid)
if (av) {
res.set('Content-Type', 'image/jpeg')
return res.send(av)
}
return res.sendStatus(404)
})
/**
* Metrics (Prometheus)
*/
router.get('/metrics', async (req, res, next) => {
if (WIKI.config.metrics.isEnabled) {
if (!WIKI.auth.checkAccess(req.user, ['read:metrics'])) {
return res.sendStatus(403)
}
WIKI.metrics.render(res)
} else {
next()
}
})
// /**
// * View document / asset
// */
// router.get('/*', async (req, res, next) => {
// const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
// const pageArgs = pageHelper.parsePath(req.path, { stripExt })
// const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
// const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
// if (!site) {
// throw new Error('INVALID_SITE')
// }
// if (isPage) {
// // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
// // return res.redirect(`/${pageArgs.locale}/${pageArgs.path}`)
// // }
// // req.i18n.changeLanguage(pageArgs.locale)
// try {
// // -> Get Page from cache
// const page = await WIKI.db.pages.getPage({
// siteId: site.id,
// path: pageArgs.path,
// locale: pageArgs.locale,
// userId: req.user.id
// })
// pageArgs.tags = _.get(page, 'tags', [])
// // -> Effective Permissions
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
// // -> Check User Access
// if (!effectivePermissions.pages.read) {
// if (req.user.id === WIKI.auth.guest.id) {
// res.cookie('loginRedirect', req.path, {
// maxAge: 15 * 60 * 1000
// })
// }
// if (pageArgs.path === 'home' && req.user.id === WIKI.auth.guest.id) {
// return res.redirect('/login')
// }
// return res.redirect(`/_error/unauthorized?from=${req.path}`)
// }
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
// // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
// if (page) {
// _.set(res.locals, 'pageMeta.title', page.title)
// _.set(res.locals, 'pageMeta.description', page.description)
// // -> Check Publishing State
// let pageIsPublished = page.isPublished
// if (pageIsPublished && !_.isEmpty(page.publishStartDate)) {
// pageIsPublished = moment(page.publishStartDate).isSameOrBefore()
// }
// if (pageIsPublished && !_.isEmpty(page.publishEndDate)) {
// pageIsPublished = moment(page.publishEndDate).isSameOrAfter()
// }
// if (!pageIsPublished && !effectivePermissions.pages.write) {
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
// return res.status(403).render('unauthorized', {
// action: 'view'
// })
// }
// // -> Render view
// res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
// } else if (pageArgs.path === 'home') {
// res.redirect('/_welcome')
// } else {
// _.set(res.locals, 'pageMeta.title', 'Page Not Found')
// if (effectivePermissions.pages.write) {
// res.status(404).render('new', { path: pageArgs.path, locale: pageArgs.locale })
// } else {
// res.status(404).render('notfound', { action: 'view' })
// }
// }
// } catch (err) {
// next(err)
// }
// } else {
// if (!WIKI.auth.checkAccess(req.user, ['read:assets'], pageArgs)) {
// return res.sendStatus(403)
// }
// await WIKI.db.assets.getAsset(pageArgs.path, res)
// }
// })
router.get('/{*splat}', async (req, res, next) => {
const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
if (!site) {
throw new Error('INVALID_SITE')
}
const stripExt = site.config.pageExtensions.some(ext => req.path.endsWith(`.${ext}`))
const pathArgs = parsePath(req.path, { stripExt })
const isPage = (stripExt || pathArgs.path.indexOf('.') === -1)
if (isPage) {
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
} else {
await WIKI.db.assets.getAsset({ pathArgs, siteId: site.id }, res)
}
})
return router
}

@ -1,39 +0,0 @@
import express from 'express'
import { get } from 'lodash-es'
import qs from 'querystring'
export default function () {
const router = express.Router()
/**
* Let's Encrypt Challenge
*/
router.get('/.well-known/acme-challenge/:token', (req, res, next) => {
res.type('text/plain')
if (get(WIKI.config, 'letsencrypt.challenge', false)) {
if (WIKI.config.letsencrypt.challenge.token === req.params.token) {
res.send(WIKI.config.letsencrypt.challenge.keyAuthorization)
WIKI.logger.info(`(LETSENCRYPT) Received valid challenge request. [ ACCEPTED ]`)
} else {
res.status(406).send('Invalid Challenge Token!')
WIKI.logger.warn(`(LETSENCRYPT) Received invalid challenge request. [ REJECTED ]`)
}
} else {
res.status(418).end()
}
})
/**
* Redirect to HTTPS if HTTP Redirection is enabled
*/
// router.all('/*', (req, res, next) => {
// if (WIKI.config.server.sslRedir && !req.secure && WIKI.servers.servers.https) {
// let query = (!_.isEmpty(req.query)) ? `?${qs.stringify(req.query)}` : ``
// return res.redirect(`https://${req.hostname}${req.originalUrl}${query}`)
// } else {
// next()
// }
// })
return router
}

@ -1,29 +0,0 @@
import chalk from 'chalk'
import os from 'node:os'
export default function () {
WIKI.servers.ws.on('connection', (socket) => {
// TODO: Validate token + permissions
const token = socket.handshake.auth.token
// console.info(token)
const listeners = {}
socket.on('server:logs', () => {
socket.emit('server:log', chalk.greenBright(`Streaming logs from ${chalk.bold('Wiki.js')} instance ${chalk.yellowBright.bold(WIKI.INSTANCE_ID)} on host ${chalk.yellowBright.bold(os.hostname())}...`))
listeners.serverLogs = (msg) => {
socket.emit('server:log', msg)
}
WIKI.logger.ws.addListener('log', listeners.serverLogs)
WIKI.logger.warn(`User XYZ is streaming server logs. ( Listeners: ${WIKI.logger.ws.listenerCount('log')} )`)
})
socket.on('disconnect', () => {
if (listeners.serverLogs) {
WIKI.logger.ws.removeListener('log', listeners.serverLogs)
delete listeners.serverLogs
}
WIKI.logger.warn(`User XYZ has stopped streaming server logs. ( Listeners: ${WIKI.logger.ws.listenerCount('log')} )`)
})
})
}

@ -1,127 +0,0 @@
import pickle from 'chromium-pickle-js'
import path from 'node:path'
import { UINT64 } from 'cuint'
import fs from 'node:fs'
/**
* Based of express-serve-asar (https://github.com/toyobayashi/express-serve-asar)
* by Fenglin Li (https://github.com/toyobayashi)
*/
export default {
fdCache: {},
async serve (pkgName, req, res, next) {
const packages = {
twemoji: path.join(WIKI.ROOTPATH, 'assets/svg/twemoji.asar')
}
const file = this.readFilesystemSync(packages[pkgName])
const { filesystem, fd } = file
const info = filesystem.getFile(req.path.substring(1))
if (info) {
res.set({
'Content-Type': 'image/svg+xml',
'Content-Length': info.size
})
fs.createReadStream('', {
fd,
autoClose: false,
start: 8 + filesystem.headerSize + parseInt(info.offset, 10),
end: 8 + filesystem.headerSize + parseInt(info.offset, 10) + info.size - 1
}).on('error', (err) => {
WIKI.logger.warn(err)
res.sendStatus(404)
}).pipe(res.status(200))
} else {
res.sendStatus(404)
}
},
async unload () {
const fds = Object.values(this.fdCache)
if (fds.length > 0) {
WIKI.logger.info('Closing ASAR file descriptors...')
const closeAsync = require('util').promisify(fs.close)
await Promise.all(fds.map(x => closeAsync(x.fd)))
this.fdCache = {}
}
},
readArchiveHeaderSync (fd) {
let size
let headerBuf
const sizeBuf = Buffer.alloc(8)
if (fs.readSync(fd, sizeBuf, 0, 8, null) !== 8) {
throw new Error('Unable to read header size')
}
const sizePickle = pickle.createFromBuffer(sizeBuf)
size = sizePickle.createIterator().readUInt32()
headerBuf = Buffer.alloc(size)
if (fs.readSync(fd, headerBuf, 0, size, null) !== size) {
throw new Error('Unable to read header')
}
const headerPickle = pickle.createFromBuffer(headerBuf)
const header = headerPickle.createIterator().readString()
return { header: JSON.parse(header), headerSize: size }
},
readFilesystemSync (archive) {
if (!this.fdCache[archive]) {
const fd = fs.openSync(archive, 'r')
const header = this.readArchiveHeaderSync(fd)
const filesystem = new Filesystem(archive)
filesystem.header = header.header
filesystem.headerSize = header.headerSize
this.fdCache[archive] = {
fd,
filesystem
}
}
return this.fdCache[archive]
}
}
class Filesystem {
constructor (src) {
this.src = path.resolve(src)
this.header = { files: {} }
this.offset = UINT64(0)
}
searchNodeFromDirectory (p) {
let json = this.header
const dirs = p.split(path.sep)
for (const dir of dirs) {
if (dir !== '.') {
json = json.files[dir]
}
}
return json
}
getNode (p) {
const node = this.searchNodeFromDirectory(path.dirname(p))
const name = path.basename(p)
if (name) {
return node.files[name]
} else {
return node
}
}
getFile (p, followLinks) {
followLinks = typeof followLinks === 'undefined' ? true : followLinks
const info = this.getNode(p)
if (!info) {
return false
}
if (info.link && followLinks) {
return this.getFile(info.link)
} else {
return info
}
}
}

@ -1,487 +0,0 @@
import passport from 'passport'
import passportJWT from 'passport-jwt'
import _ from 'lodash'
import jwt from 'jsonwebtoken'
import ms from 'ms'
import { DateTime } from 'luxon'
import util from 'node:util'
import crypto from 'node:crypto'
import { pem2jwk } from 'pem-jwk'
import NodeCache from 'node-cache'
import { extractJWT } from '../helpers/security.mjs'
const randomBytes = util.promisify(crypto.randomBytes)
export default {
strategies: {},
guest: {
cacheExpiration: DateTime.utc().minus({ days: 1 })
},
groups: {},
validApiKeys: [],
revocationList: new NodeCache(),
/**
* Initialize the authentication module
*/
init() {
this.passport = passport
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser(async (id, done) => {
try {
const user = await WIKI.db.users.query().findById(id).withGraphFetched('groups').modifyGraph('groups', builder => {
builder.select('groups.id', 'permissions')
})
if (user) {
done(null, user)
} else {
done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null)
}
} catch (err) {
done(err, null)
}
})
this.reloadGroups()
this.reloadApiKeys()
return this
},
/**
* Load authentication strategies
*/
async activateStrategies () {
try {
// Unload any active strategies
WIKI.auth.strategies = {}
const currentStrategies = _.keys(passport._strategies)
_.pull(currentStrategies, 'session')
_.forEach(currentStrategies, stg => { passport.unuse(stg) })
// Load JWT
passport.use('jwt', new passportJWT.Strategy({
jwtFromRequest: extractJWT,
secretOrKey: WIKI.config.auth.certs.public,
audience: WIKI.config.auth.audience,
issuer: 'urn:wiki.js',
algorithms: ['RS256']
}, (jwtPayload, cb) => {
cb(null, jwtPayload)
}))
// Load enabled strategies
const enabledStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
for (const stg of enabledStrategies) {
try {
const strategy = (await import(`../modules/authentication/${stg.module}/authentication.mjs`)).default
strategy.init(passport, stg.id, stg.config)
WIKI.auth.strategies[stg.id] = {
...strategy,
...stg
}
WIKI.logger.info(`Authentication Strategy ${stg.displayName}: [ OK ]`)
} catch (err) {
WIKI.logger.error(`Authentication Strategy ${stg.displayName} (${stg.id}): [ FAILED ]`)
WIKI.logger.error(err)
}
}
} catch (err) {
WIKI.logger.error(`Failed to initialize Authentication Strategies: [ ERROR ]`)
WIKI.logger.error(err)
}
},
/**
* Authenticate current request
*
* @param {Express Request} req
* @param {Express Response} res
* @param {Express Next Callback} next
*/
authenticate (req, res, next) {
req.isAuthenticated = false
WIKI.auth.passport.authenticate('jwt', { session: false }, async (err, user, info) => {
if (err) { return next() }
let mustRevalidate = false
const strategyId = user.pvd
// Expired but still valid within N days, just renew
if (info instanceof Error && info.name === 'TokenExpiredError') {
const expiredDate = (info.expiredAt instanceof Date) ? info.expiredAt.toISOString() : info.expiredAt
if (DateTime.utc().minus(ms(WIKI.config.auth.tokenRenewal)) < DateTime.fromISO(expiredDate)) {
mustRevalidate = true
}
}
// Check if user / group is in revocation list
if (user && !user.api && !mustRevalidate) {
const uRevalidate = WIKI.auth.revocationList.get(`u${_.toString(user.id)}`)
if (uRevalidate && user.iat < uRevalidate) {
mustRevalidate = true
} else if (DateTime.fromSeconds(user.iat) <= WIKI.startedAt) { // Prevent new / restarted instance from allowing revoked tokens
mustRevalidate = true
} else {
for (const gid of user.groups) {
const gRevalidate = WIKI.auth.revocationList.get(`g${_.toString(gid)}`)
if (gRevalidate && user.iat < gRevalidate) {
mustRevalidate = true
break
}
}
}
}
// Revalidate and renew token
if (mustRevalidate && !req.path.startsWith('/_graphql')) {
const jwtPayload = jwt.decode(extractJWT(req))
try {
const newToken = await WIKI.db.users.refreshToken(jwtPayload.id, jwtPayload.pvd)
user = newToken.user
user.permissions = user.getPermissions()
user.groups = user.getGroups()
user.strategyId = strategyId
req.user = user
// Try headers, otherwise cookies for response
if (req.get('content-type') === 'application/json') {
res.set('new-jwt', newToken.token)
} else {
res.cookie('jwt', newToken.token, { expires: DateTime.utc().plus({ days: 365 }).toJSDate() })
}
} catch (errc) {
WIKI.logger.warn(errc)
return next()
}
} else if (user) {
user = await WIKI.db.users.getById(user.id)
user.permissions = user.getPermissions()
user.groups = user.getGroups()
user.strategyId = strategyId
req.user = user
} else {
// JWT is NOT valid, set as guest
if (WIKI.auth.guest.cacheExpiration <= DateTime.utc()) {
WIKI.auth.guest = await WIKI.db.users.getGuestUser()
WIKI.auth.guest.cacheExpiration = DateTime.utc().plus({ minutes: 1 })
}
req.user = WIKI.auth.guest
req.isAuthenticated = false
return next()
}
// Process API tokens
if (_.has(user, 'api')) {
if (!WIKI.config.api.isEnabled) {
return next(new Error('API is disabled. You must enable it from the Administration Area first.'))
} else if (_.includes(WIKI.auth.validApiKeys, user.api)) {
req.user = {
id: 1,
email: 'api@localhost',
name: 'API',
pictureUrl: null,
timezone: 'America/New_York',
locale: 'en',
permissions: _.get(WIKI.auth.groups, `${user.grp}.permissions`, []),
groups: [user.grp],
getPermissions () {
return req.user.permissions
},
getGroups () {
return req.user.groups
}
}
return next()
} else {
return next(new Error('API Key is invalid or was revoked.'))
}
}
// JWT is valid
req.logIn(user, { session: false }, (errc) => {
if (errc) { return next(errc) }
req.isAuthenticated = true
next()
})
})(req, res, next)
},
/**
* Check if user has access to resource
*
* @param {User} user
* @param {Array<String>} permissions
* @param {String|Boolean} path
*/
checkAccess(user, permissions = [], page = false) {
const userPermissions = user.permissions ? user.permissions : user.getPermissions()
// System Admin
if (_.includes(userPermissions, 'manage:system')) {
return true
}
// Check Permissions
if (_.intersection(userPermissions, permissions).length < 1) {
return false
}
// Skip if no page rule to check
if (!page) {
return true
}
// Check Page Rules
if (user.groups) {
let checkState = {
deny: false,
match: false,
specificity: ''
}
user.groups.forEach(grp => {
const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp
_.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {
if (rule.locales && rule.locales.length > 0) {
if (!rule.locales.includes(page.locale)) { return }
}
if (_.intersection(rule.roles, permissions).length > 0) {
switch (rule.match) {
case 'START':
if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {
checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT', 'TAG'] })
}
break
case 'END':
if (_.endsWith(page.path, rule.path)) {
checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT', 'TAG'] })
}
break
case 'REGEX':
const reg = new RegExp(rule.path)
if (reg.test(page.path)) {
checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT', 'TAG'] })
}
break
case 'TAG':
_.get(page, 'tags', []).forEach(tag => {
if (tag.tag === rule.path) {
checkState = this._applyPageRuleSpecificity({
rule,
checkState,
higherPriority: ['EXACT']
})
}
})
break
case 'EXACT':
if (`/${page.path}` === `/${rule.path}`) {
checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })
}
break
}
}
})
})
return (checkState.match && !checkState.deny)
}
return false
},
/**
* Check for exclusive permissions (contain any X permission(s) but not any Y permission(s))
*
* @param {User} user
* @param {Array<String>} includePermissions
* @param {Array<String>} excludePermissions
*/
checkExclusiveAccess(user, includePermissions = [], excludePermissions = []) {
const userPermissions = user.permissions ? user.permissions : user.getPermissions()
// Check Inclusion Permissions
if (_.intersection(userPermissions, includePermissions).length < 1) {
return false
}
// Check Exclusion Permissions
if (_.intersection(userPermissions, excludePermissions).length > 0) {
return false
}
return true
},
/**
* Check and apply Page Rule specificity
*
* @access private
*/
_applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {
if (rule.path.length === checkState.specificity.length) {
// Do not override higher priority rules
if (_.includes(higherPriority, checkState.match)) {
return checkState
}
// Do not override a previous DENY rule with same match
if (rule.match === checkState.match && checkState.deny && !rule.deny) {
return checkState
}
} else if (rule.path.length < checkState.specificity.length) {
// Do not override higher specificity rules
return checkState
}
return {
deny: rule.deny,
match: rule.match,
specificity: rule.path
}
},
/**
* Reload Groups from DB
*/
async reloadGroups () {
const groupsArray = await WIKI.db.groups.query()
this.groups = _.keyBy(groupsArray, 'id')
WIKI.auth.guest.cacheExpiration = DateTime.utc().minus({ days: 1 })
},
/**
* Reload valid API Keys from DB
*/
async reloadApiKeys () {
const keys = await WIKI.db.apiKeys.query().select('id').where('isRevoked', false).andWhere('expiration', '>', DateTime.utc().toISO())
this.validApiKeys = _.map(keys, 'id')
},
/**
* Generate New Authentication Public / Private Key Certificates
*/
async regenerateCertificates () {
WIKI.logger.info('Regenerating certificates...')
_.set(WIKI.config, 'sessionSecret', (await randomBytes(32)).toString('hex'))
const certs = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: WIKI.config.sessionSecret
}
})
_.set(WIKI.config, 'certs', {
jwk: pem2jwk(certs.publicKey),
public: certs.publicKey,
private: certs.privateKey
})
await WIKI.configSvc.saveToDb([
'certs',
'sessionSecret'
])
await WIKI.auth.activateStrategies()
WIKI.events.outbound.emit('reloadAuthStrategies')
WIKI.logger.info('Regenerated certificates: [ COMPLETED ]')
},
/**
* Reset Guest User
*/
async resetGuestUser() {
WIKI.logger.info('Resetting guest account...')
const guestGroup = await WIKI.db.groups.query().where('id', 2).first()
await WIKI.db.users.query().delete().where({
providerKey: 'local',
email: 'guest@example.com'
}).orWhere('id', 2)
const guestUser = await WIKI.db.users.query().insert({
id: 2,
provider: 'local',
email: 'guest@example.com',
name: 'Guest',
password: '',
locale: 'en',
defaultEditor: 'markdown',
tfaIsActive: false,
isSystem: true,
isActive: true,
isVerified: true
})
await guestUser.$relatedQuery('groups').relate(guestGroup.id)
WIKI.logger.info('Guest user has been reset: [ COMPLETED ]')
},
/**
* Subscribe to HA propagation events
*/
subscribeToEvents() {
WIKI.events.inbound.on('reloadGroups', () => {
WIKI.auth.reloadGroups()
})
WIKI.events.inbound.on('reloadApiKeys', () => {
WIKI.auth.reloadApiKeys()
})
WIKI.events.inbound.on('reloadAuthStrategies', () => {
WIKI.auth.activateStrategies()
})
WIKI.events.inbound.on('addAuthRevoke', (args) => {
WIKI.auth.revokeUserTokens(args)
})
},
/**
* Get all user permissions for a specific page
*/
getEffectivePermissions (req, page) {
return {
comments: {
read: WIKI.auth.checkAccess(req.user, ['read:comments'], page),
write: WIKI.auth.checkAccess(req.user, ['write:comments'], page),
manage: WIKI.auth.checkAccess(req.user, ['manage:comments'], page)
},
history: {
read: WIKI.auth.checkAccess(req.user, ['read:history'], page)
},
source: {
read: WIKI.auth.checkAccess(req.user, ['read:source'], page)
},
pages: {
read: WIKI.auth.checkAccess(req.user, ['read:pages'], page),
write: WIKI.auth.checkAccess(req.user, ['write:pages'], page),
manage: WIKI.auth.checkAccess(req.user, ['manage:pages'], page),
delete: WIKI.auth.checkAccess(req.user, ['delete:pages'], page),
script: WIKI.auth.checkAccess(req.user, ['write:scripts'], page),
style: WIKI.auth.checkAccess(req.user, ['write:styles'], page)
},
system: {
manage: WIKI.auth.checkAccess(req.user, ['manage:system'], page)
}
}
},
/**
* Add user / group ID to JWT revocation list, forcing all requests to be validated against the latest permissions
*/
revokeUserTokens ({ id, kind = 'u' }) {
WIKI.auth.revocationList.set(`${kind}${_.toString(id)}`, Math.round(DateTime.utc().minus({ seconds: 5 }).toSeconds()), Math.ceil(ms(WIKI.config.auth.tokenExpiration) / 1000))
}
}

@ -1,148 +0,0 @@
import { defaultsDeep, get, isPlainObject } from 'lodash-es'
import chalk from 'chalk'
import cfgHelper from '../helpers/config.mjs'
import regexData from '../app/regex.mjs'
import fs from 'node:fs/promises'
import path from 'node:path'
import yaml from 'js-yaml'
export default {
/**
* Load root config from disk
*/
async init(silent = false) {
const confPaths = {
config: path.join(WIKI.ROOTPATH, 'config.yml'),
data: path.join(WIKI.SERVERPATH, 'app/data.yml')
}
if (process.env.dockerdev) {
confPaths.config = path.join(WIKI.ROOTPATH, `dev/containers/config.yml`)
}
if (process.env.CONFIG_FILE) {
confPaths.config = path.resolve(WIKI.ROOTPATH, process.env.CONFIG_FILE)
}
if (!silent) {
process.stdout.write(chalk.blue(`Loading configuration from ${confPaths.config}... `))
}
let appconfig = {}
let appdata = {}
try {
appconfig = yaml.load(
cfgHelper.parseConfigValue(
await fs.readFile(confPaths.config, 'utf8')
)
)
appdata = yaml.load(await fs.readFile(confPaths.data, 'utf8'))
appdata.regex = regexData
if (!silent) {
console.info(chalk.green.bold(`OK`))
}
} catch (err) {
console.error(chalk.red.bold(`FAILED`))
console.error(err.message)
console.error(chalk.red.bold(`>>> Unable to read configuration file! Did you create the config.yml file?`))
process.exit(1)
}
// Merge with defaults
appconfig = defaultsDeep(appconfig, appdata.defaults.config)
// Override port
if (appconfig.port < 1 || process.env.HEROKU) {
appconfig.port = process.env.PORT || 80
}
if (process.env.WIKI_PORT) {
appconfig.port = process.env.WIKI_PORT || 80
}
// Load package info
const packageInfo = JSON.parse(await fs.readFile(path.join(WIKI.SERVERPATH, 'package.json'), 'utf-8'))
// Load DB Password from Docker Secret File
if (process.env.DB_PASS_FILE) {
if (!silent) {
console.info(chalk.blue(`DB_PASS_FILE is defined. Will use secret from file.`))
}
try {
appconfig.db.pass = await fs.readFile(process.env.DB_PASS_FILE, 'utf8').trim()
} catch (err) {
console.error(chalk.red.bold(`>>> Failed to read Docker Secret File using path defined in DB_PASS_FILE env variable!`))
console.error(err.message)
process.exit(1)
}
}
WIKI.config = appconfig
WIKI.data = appdata
WIKI.version = packageInfo.version
WIKI.releaseDate = packageInfo.releaseDate
WIKI.devMode = (packageInfo.dev === true)
},
/**
* Load config from DB
*/
async loadFromDb() {
const conf = await WIKI.db.settings.getConfig()
if (conf) {
WIKI.config = defaultsDeep(conf, WIKI.config)
} else {
WIKI.logger.warn('Missing DB Configuration!')
process.exit(1)
}
},
/**
* Save config to DB
*
* @param {Array} keys Array of keys to save
* @returns Promise
*/
async saveToDb(keys, propagate = true) {
try {
for (const key of keys) {
let value = get(WIKI.config, key, null)
if (!isPlainObject(value)) {
value = { v: value }
}
let affectedRows = await WIKI.db.settings.query().patch({ value }).where('key', key)
if (affectedRows === 0 && value) {
await WIKI.db.settings.query().insert({ key, value })
}
}
if (propagate) {
WIKI.events.outbound.emit('reloadConfig')
}
} catch (err) {
WIKI.logger.error(`Failed to save configuration to DB: ${err.message}`)
return false
}
return true
},
/**
* Apply Dev Flags
*/
async applyFlags() {
WIKI.db.knex.client.config.debug = WIKI.config.flags.sqlLog
},
/**
* Subscribe to HA propagation events
*/
subscribeToEvents() {
WIKI.events.inbound.on('reloadConfig', async () => {
await WIKI.configSvc.loadFromDb()
await WIKI.configSvc.applyFlags()
})
}
}

@ -1,243 +0,0 @@
import { create, get, has, isEmpty, isPlainObject } from 'lodash-es'
import path from 'node:path'
import knex from 'knex'
import fs from 'node:fs/promises'
import Objection from 'objection'
import PGPubSub from 'pg-pubsub'
import semver from 'semver'
import { createDeferred } from '../helpers/common.mjs'
import migrationSource from '../db/migrator-source.mjs'
// const migrateFromLegacy = require('../db/legacy')
import { setTimeout } from 'node:timers/promises'
/**
* ORM DB module
*/
export default {
Objection,
knex: null,
listener: null,
config: null,
VERSION: null,
LEGACY: false,
onReady: createDeferred(),
connectAttempts: 0,
/**
* Initialize DB
*/
async init (workerMode = false) {
WIKI.logger.info('Checking DB configuration...')
// Fetch DB Config
this.config = (!isEmpty(process.env.DATABASE_URL))
? process.env.DATABASE_URL
: {
host: WIKI.config.db.host.toString(),
user: WIKI.config.db.user.toString(),
password: WIKI.config.db.pass.toString(),
database: WIKI.config.db.db.toString(),
port: WIKI.config.db.port
}
// Handle SSL Options
let dbUseSSL = (WIKI.config.db.ssl === true || WIKI.config.db.ssl === 'true' || WIKI.config.db.ssl === 1 || WIKI.config.db.ssl === '1')
let sslOptions = null
if (dbUseSSL && isPlainObject(this.config) && get(WIKI.config.db, 'sslOptions.auto', null) === false) {
sslOptions = WIKI.config.db.sslOptions
sslOptions.rejectUnauthorized = sslOptions.rejectUnauthorized !== false
if (sslOptions.ca && sslOptions.ca.indexOf('-----') !== 0) {
sslOptions.ca = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.ca), 'utf-8')
}
if (sslOptions.cert) {
sslOptions.cert = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.cert), 'utf-8')
}
if (sslOptions.key) {
sslOptions.key = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.key), 'utf-8')
}
if (sslOptions.pfx) {
sslOptions.pfx = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.pfx), 'utf-8')
}
} else {
sslOptions = true
}
// Handle inline SSL CA Certificate mode
if (!isEmpty(process.env.DB_SSL_CA)) {
const chunks = []
for (let i = 0, charsLength = process.env.DB_SSL_CA.length; i < charsLength; i += 64) {
chunks.push(process.env.DB_SSL_CA.substring(i, i + 64))
}
dbUseSSL = true
sslOptions = {
rejectUnauthorized: true,
ca: '-----BEGIN CERTIFICATE-----\n' + chunks.join('\n') + '\n-----END CERTIFICATE-----\n'
}
}
if (dbUseSSL && isPlainObject(this.config)) {
this.config.ssl = (sslOptions === true) ? { rejectUnauthorized: true } : sslOptions
}
// Initialize Knex
this.knex = knex({
client: 'pg',
useNullAsDefault: true,
asyncStackTraces: WIKI.IS_DEBUG,
connection: this.config,
searchPath: [WIKI.config.db.schema],
pool: {
...workerMode ? { min: 0, max: 1 } : WIKI.config.pool
// FIXME: Throws DeprecatingWarning because Knex encapsulates this into a promisify...
// async afterCreate (conn, done) {
// // -> Set Connection App Name
// if (workerMode) {
// await conn.query(`set application_name = 'Wiki.js - ${WIKI.INSTANCE_ID}'`)
// } else {
// await conn.query(`set application_name = 'Wiki.js - ${WIKI.INSTANCE_ID}:MAIN'`)
// }
// done()
// }
},
debug: WIKI.IS_DEBUG
})
Objection.Model.knex(this.knex)
// Load DB Models
WIKI.logger.info('Loading DB models...')
const models = (await import(path.join(WIKI.SERVERPATH, 'models/index.mjs'))).default
// Connect
await this.connect()
// Check DB Version
const resVersion = await this.knex.raw('SHOW server_version;')
const dbVersion = semver.coerce(resVersion.rows[0].server_version, { loose: true })
this.VERSION = dbVersion.version
this.LEGACY = dbVersion.major < 16
if (dbVersion.major < 12) {
WIKI.logger.error(`Your PostgreSQL database version (${dbVersion.major}) is too old and unsupported by Wiki.js. Requires >= 12. Exiting...`)
process.exit(1)
}
WIKI.logger.info(`PostgreSQL ${dbVersion.version} [ ${this.LEGACY ? 'LEGACY MODE' : 'OK'} ]`)
// Run Migrations
if (!workerMode) {
await this.migrateFromLegacy()
await this.syncSchemas()
}
return {
...this,
...models
}
},
/**
* Subscribe to database LISTEN / NOTIFY for multi-instances events
*/
async subscribeToNotifications () {
let connSettings = this.knex.client.connectionSettings
if (typeof connSettings === 'string') {
const encodedName = encodeURIComponent(`Wiki.js - ${WIKI.INSTANCE_ID}:PSUB`)
if (connSettings.indexOf('?') > 0) {
connSettings = `${connSettings}&ApplicationName=${encodedName}`
} else {
connSettings = `${connSettings}?ApplicationName=${encodedName}`
}
} else {
connSettings.application_name = `Wiki.js - ${WIKI.INSTANCE_ID}:PSUB`
}
this.listener = new PGPubSub(connSettings, {
log (ev) {
WIKI.logger.debug(ev)
}
})
// -> Outbound events handling
this.listener.addChannel('wiki', payload => {
if (has(payload, 'event') && payload.source !== WIKI.INSTANCE_ID) {
WIKI.logger.info(`Received event ${payload.event} from instance ${payload.source}: [ OK ]`)
WIKI.events.inbound.emit(payload.event, payload.value)
}
})
WIKI.events.outbound.onAny(this.notifyViaDB)
// -> Listen to inbound events
WIKI.auth.subscribeToEvents()
WIKI.configSvc.subscribeToEvents()
WIKI.db.pages.subscribeToEvents()
WIKI.logger.info('PG PubSub Listener initialized successfully: [ OK ]')
},
/**
* Unsubscribe from database LISTEN / NOTIFY
*/
async unsubscribeToNotifications () {
if (this.listener) {
WIKI.events.outbound.offAny(this.notifyViaDB)
WIKI.events.inbound.removeAllListeners()
this.listener.close()
}
},
/**
* Publish event via database NOTIFY
*
* @param {string} event Event fired
* @param {object} value Payload of the event
*/
notifyViaDB (event, value) {
WIKI.db.listener.publish('wiki', {
source: WIKI.INSTANCE_ID,
event,
value
})
},
/**
* Attempt initial connection
*/
async connect () {
try {
WIKI.logger.info('Connecting to database...')
await this.knex.raw('SELECT 1 + 1;')
WIKI.logger.info('Database Connection Successful [ OK ]')
} catch (err) {
if (this.connectAttempts < 10) {
if (err.code) {
WIKI.logger.error(`Database Connection Error: ${err.code} ${err.address}:${err.port}`)
} else {
WIKI.logger.error(`Database Connection Error: ${err.message}`)
}
WIKI.logger.warn(`Will retry in 3 seconds... [Attempt ${++this.connectAttempts} of 10]`)
await setTimeout(3000)
await this.connect()
} else {
throw err
}
}
},
/**
* Migrate DB Schemas
*/
async syncSchemas () {
WIKI.logger.info('Ensuring DB schema exists...')
await this.knex.raw(`CREATE SCHEMA IF NOT EXISTS ${WIKI.config.db.schema}`)
WIKI.logger.info('Ensuring DB migrations have been applied...')
return this.knex.migrate.latest({
tableName: 'migrations',
migrationSource,
schemaName: WIKI.config.db.schema
})
},
/**
* Migrate DB Schemas from 2.x
*/
async migrateFromLegacy () {
// return migrateFromLegacy.migrate(self.knex)
}
}

@ -1,19 +0,0 @@
import fs from 'node:fs/promises'
import path from 'path'
export default {
ext: {},
async init () {
const extDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/extensions'))
WIKI.logger.info(`Checking for installed optional extensions...`)
for (const dir of extDirs) {
WIKI.extensions.ext[dir] = (await import(path.join(WIKI.SERVERPATH, 'modules/extensions', dir, 'ext.mjs'))).default
const isInstalled = await WIKI.extensions.ext[dir].check()
if (isInstalled) {
WIKI.logger.info(`Optional extension ${dir} is installed. [ OK ]`)
} else {
WIKI.logger.info(`Optional extension ${dir} was not found on this system. [ SKIPPED ]`)
}
}
}
}

@ -1,119 +0,0 @@
import { padEnd } from 'lodash-es'
import eventemitter2 from 'eventemitter2'
import NodeCache from 'node-cache'
import asar from './asar.mjs'
import db from './db.mjs'
import extensions from './extensions.mjs'
import scheduler from './scheduler.mjs'
import servers from './servers.mjs'
import metrics from './metrics.mjs'
let isShuttingDown = false
export default {
async init() {
WIKI.logger.info('=======================================')
WIKI.logger.info(`= Wiki.js ${padEnd(WIKI.version + ' ', 29, '=')}`)
WIKI.logger.info('=======================================')
WIKI.logger.info('Initializing...')
WIKI.logger.info(`Running node.js ${process.version}`)
WIKI.db = await db.init()
try {
await WIKI.configSvc.loadFromDb()
await WIKI.configSvc.applyFlags()
} catch (err) {
WIKI.logger.error('Database Initialization Error: ' + err.message)
if (WIKI.IS_DEBUG) {
WIKI.logger.error(err)
}
process.exit(1)
}
this.bootWeb()
},
/**
* Pre-Web Boot Sequence
*/
async preBootWeb() {
try {
WIKI.cache = new NodeCache({ checkperiod: 0 })
WIKI.scheduler = await scheduler.init()
WIKI.servers = servers
WIKI.events = {
inbound: new eventemitter2.EventEmitter2(),
outbound: new eventemitter2.EventEmitter2()
}
WIKI.extensions = extensions
WIKI.asar = asar
WIKI.metrics = await metrics.init()
} catch (err) {
WIKI.logger.error(err)
process.exit(1)
}
},
/**
* Boot Web Process
*/
async bootWeb() {
try {
await this.preBootWeb()
await (await import('../web.mjs')).init()
this.postBootWeb()
} catch (err) {
WIKI.logger.error(err)
process.exit(1)
}
},
/**
* Post-Web Boot Sequence
*/
async postBootWeb() {
await WIKI.db.locales.refreshFromDisk()
await WIKI.db.analytics.refreshProvidersFromDisk()
await WIKI.db.authentication.refreshStrategiesFromDisk()
await WIKI.db.commentProviders.refreshProvidersFromDisk()
await WIKI.db.renderers.refreshRenderersFromDisk()
await WIKI.db.storage.refreshTargetsFromDisk()
await WIKI.extensions.init()
await WIKI.auth.activateStrategies()
await WIKI.db.commentProviders.initProvider()
await WIKI.db.locales.reloadCache()
await WIKI.db.sites.reloadCache()
await WIKI.db.storage.initTargets()
await WIKI.db.subscribeToNotifications()
await WIKI.scheduler.start()
},
/**
* Graceful shutdown
*/
async shutdown (devMode = false) {
if (isShuttingDown) { return }
isShuttingDown = true
if (WIKI.servers) {
await WIKI.servers.stopServers()
}
if (WIKI.scheduler) {
await WIKI.scheduler.stop()
}
if (WIKI.models) {
await WIKI.db.unsubscribeToNotifications()
if (WIKI.db.knex) {
await WIKI.db.knex.destroy()
}
}
if (WIKI.asar) {
await WIKI.asar.unload()
}
if (!devMode) {
WIKI.logger.info('Terminating process...')
process.exit(0)
}
}
}

@ -1,122 +0,0 @@
const ACME = require('acme')
const Keypairs = require('@root/keypairs')
const _ = require('lodash')
const moment = require('moment')
const CSR = require('@root/csr')
const PEM = require('@root/pem')
const punycode = require('punycode/')
module.exports = {
apiDirectory: WIKI.dev ? 'https://acme-staging-v02.api.letsencrypt.org/directory' : 'https://acme-v02.api.letsencrypt.org/directory',
acme: null,
async init () {
if (!_.get(WIKI.config, 'letsencrypt.payload', false)) {
await this.requestCertificate()
} else if (WIKI.config.letsencrypt.domain !== WIKI.config.ssl.domain) {
WIKI.logger.info(`(LETSENCRYPT) Domain has changed. Requesting new certificates...`)
await this.requestCertificate()
} else if (moment(WIKI.config.letsencrypt.payload.expires).isSameOrBefore(moment().add(5, 'days'))) {
WIKI.logger.info(`(LETSENCRYPT) Certificate is about to or has expired, requesting a new one...`)
await this.requestCertificate()
} else {
WIKI.logger.info(`(LETSENCRYPT) Using existing certificate for ${WIKI.config.ssl.domain}, expires on ${WIKI.config.letsencrypt.payload.expires}: [ OK ]`)
}
WIKI.config.ssl.format = 'pem'
WIKI.config.ssl.inline = true
WIKI.config.ssl.key = WIKI.config.letsencrypt.serverKey
WIKI.config.ssl.cert = WIKI.config.letsencrypt.payload.cert + '\n' + WIKI.config.letsencrypt.payload.chain
WIKI.config.ssl.passphrase = null
WIKI.config.ssl.dhparam = null
},
async requestCertificate () {
try {
WIKI.logger.info(`(LETSENCRYPT) Initializing Let's Encrypt client...`)
this.acme = ACME.create({
maintainerEmail: WIKI.config.maintainerEmail,
packageAgent: `wikijs/${WIKI.version}`,
notify: (ev, msg) => {
if (_.includes(['warning', 'error'], ev)) {
WIKI.logger.warn(`${ev}: ${msg}`)
} else {
WIKI.logger.debug(`${ev}: ${JSON.stringify(msg)}`)
}
}
})
await this.acme.init(this.apiDirectory)
// -> Create ACME Subscriber account
if (!_.get(WIKI.config, 'letsencrypt.account', false)) {
WIKI.logger.info(`(LETSENCRYPT) Setting up account for the first time...`)
const accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' })
const account = await this.acme.accounts.create({
subscriberEmail: WIKI.config.ssl.subscriberEmail,
agreeToTerms: true,
accountKey: accountKeypair.private
})
WIKI.config.letsencrypt = {
accountKeypair: accountKeypair,
account: account,
domain: WIKI.config.ssl.domain
}
await WIKI.configSvc.saveToDb(['letsencrypt'])
WIKI.logger.info(`(LETSENCRYPT) Account was setup successfully [ OK ]`)
}
// -> Create Server Keypair
if (!WIKI.config.letsencrypt.serverKey) {
WIKI.logger.info(`(LETSENCRYPT) Generating server keypairs...`)
const serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' })
WIKI.config.letsencrypt.serverKey = await Keypairs.export({ jwk: serverKeypair.private })
WIKI.logger.info(`(LETSENCRYPT) Server keypairs generated successfully [ OK ]`)
}
// -> Create CSR
WIKI.logger.info(`(LETSENCRYPT) Generating certificate signing request (CSR)...`)
const domains = [ punycode.toASCII(WIKI.config.ssl.domain) ]
const serverKey = await Keypairs.import({ pem: WIKI.config.letsencrypt.serverKey })
const csrDer = await CSR.csr({ jwk: serverKey, domains, encoding: 'der' })
const csr = PEM.packBlock({ type: 'CERTIFICATE REQUEST', bytes: csrDer })
WIKI.logger.info(`(LETSENCRYPT) CSR generated successfully [ OK ]`)
// -> Verify Domain + Get Certificate
WIKI.logger.info(`(LETSENCRYPT) Requesting certificate from Let's Encrypt...`)
const certResp = await this.acme.certificates.create({
account: WIKI.config.letsencrypt.account,
accountKey: WIKI.config.letsencrypt.accountKeypair.private,
csr,
domains,
challenges: {
'http-01': {
init () {},
set (data) {
WIKI.logger.info(`(LETSENCRYPT) Setting HTTP challenge for ${data.challenge.hostname}: [ READY ]`)
WIKI.config.letsencrypt.challenge = data.challenge
WIKI.logger.info(`(LETSENCRYPT) Waiting for challenge to complete...`)
return null // <- this is needed, cannot be undefined
},
get (data) {
return WIKI.config.letsencrypt.challenge
},
async remove (data) {
WIKI.logger.info(`(LETSENCRYPT) Removing HTTP challenge: [ OK ]`)
WIKI.config.letsencrypt.challenge = null
return null // <- this is needed, cannot be undefined
}
}
}
})
WIKI.logger.info(`(LETSENCRYPT) New certifiate received successfully: [ COMPLETED ]`)
WIKI.config.letsencrypt.payload = certResp
WIKI.config.letsencrypt.domain = WIKI.config.ssl.domain
await WIKI.configSvc.saveToDb(['letsencrypt'])
} catch (err) {
WIKI.logger.warn(`(LETSENCRYPT) ${err}`)
throw err
}
}
}

@ -1,61 +0,0 @@
import chalk from 'chalk'
import EventEmitter from 'node:events'
const LEVELS = ['error', 'warn', 'info', 'debug']
const LEVELSIGNORED = ['verbose', 'silly']
const LEVELCOLORS = {
error: 'red',
warn: 'yellow',
info: 'green',
debug: 'cyan'
}
class Logger extends EventEmitter {}
export default {
loggers: {},
init () {
const primaryLogger = new Logger()
let ignoreNextLevels = false
primaryLogger.ws = new EventEmitter()
LEVELS.forEach(lvl => {
primaryLogger[lvl] = (...args) => {
primaryLogger.emit(lvl, ...args)
}
if (!ignoreNextLevels) {
primaryLogger.on(lvl, (msg) => {
let formatted = ''
if (WIKI.config.logFormat === 'json') {
formatted = JSON.stringify({
timestamp: new Date().toISOString(),
instance: WIKI.INSTANCE_ID,
level: lvl,
message: msg
})
} else {
if (msg instanceof Error) {
msg = msg.stack
}
formatted = `${new Date().toISOString()} ${chalk.dim('[' + WIKI.INSTANCE_ID + ']')} ${chalk[LEVELCOLORS[lvl]].bold(lvl)}: ${msg}`
}
console.log(formatted)
primaryLogger.ws.emit('log', formatted)
})
}
if (lvl === WIKI.config.logLevel) {
ignoreNextLevels = true
}
})
LEVELSIGNORED.forEach(lvl => {
primaryLogger[lvl] = () => {}
})
return primaryLogger
}
}

@ -1,79 +0,0 @@
import nodemailer from 'nodemailer'
import { get } from 'lodash-es'
import path from 'node:path'
import { config } from '@vue-email/compiler'
export default {
vueEmail: null,
transport: null,
templates: {},
init() {
if (get(WIKI.config, 'mail.host', '').length > 2) {
let conf = {
host: WIKI.config.mail.host,
port: WIKI.config.mail.port,
name: WIKI.config.mail.name,
secure: WIKI.config.mail.secure,
tls: {
rejectUnauthorized: !(WIKI.config.mail.verifySSL === false)
}
}
if (get(WIKI.config, 'mail.user', '').length > 1) {
conf = {
...conf,
auth: {
user: WIKI.config.mail.user,
pass: WIKI.config.mail.pass
}
}
}
if (get(WIKI.config, 'mail.useDKIM', false)) {
conf = {
...conf,
dkim: {
domainName: WIKI.config.mail.dkimDomainName,
keySelector: WIKI.config.mail.dkimKeySelector,
privateKey: WIKI.config.mail.dkimPrivateKey
}
}
}
this.transport = nodemailer.createTransport(conf)
this.vueEmail = config(path.join(WIKI.SERVERPATH, 'templates/mail'), {
verbose: false,
options: {
baseUrl: WIKI.config.mail.defaultBaseURL
}
})
} else {
WIKI.logger.warn('Mail is not setup! Please set the configuration in the administration area!')
this.transport = null
}
return this
},
async send(opts) {
if (!this.transport) {
WIKI.logger.warn('Cannot send email because mail is not setup in the administration area!')
throw new Error('ERR_MAIL_NOT_CONFIGURED')
}
return this.transport.sendMail({
headers: {
'x-mailer': 'Wiki.js'
},
from: `"${WIKI.config.mail.senderName}" <${WIKI.config.mail.senderEmail}>`,
to: opts.to,
subject: opts.subject,
text: opts.text,
html: await this.loadTemplate(opts.template, opts.data)
})
},
async loadTemplate(key, opts = {}) {
try {
return this.vueEmail.render(`${key}.vue`, {
props: opts
})
} catch (err) {
WIKI.logger.warn(err)
throw new Error('ERR_MAIL_RENDER_FAILED')
}
}
}

@ -1,66 +0,0 @@
import { collectDefaultMetrics, register, Gauge } from 'prom-client'
import { toSafeInteger } from 'lodash-es'
export default {
customMetrics: {},
async init () {
if (WIKI.config.metrics.isEnabled) {
WIKI.logger.info('Initializing metrics...')
register.setDefaultLabels({
WIKI_INSTANCE: WIKI.INSTANCE_ID
})
collectDefaultMetrics()
this.customMetrics.groupsTotal = new Gauge({
name: 'wiki_groups_total',
help: 'Total number of groups',
async collect() {
const total = await WIKI.db.groups.query().count('* as total').first()
this.set(toSafeInteger(total.total))
}
})
this.customMetrics.pagesTotal = new Gauge({
name: 'wiki_pages_total',
help: 'Total number of pages',
async collect() {
const total = await WIKI.db.pages.query().count('* as total').first()
this.set(toSafeInteger(total.total))
}
})
this.customMetrics.tagsTotal = new Gauge({
name: 'wiki_tags_total',
help: 'Total number of tags',
async collect() {
const total = await WIKI.db.tags.query().count('* as total').first()
this.set(toSafeInteger(total.total))
}
})
this.customMetrics.usersTotal = new Gauge({
name: 'wiki_users_total',
help: 'Total number of users',
async collect() {
const total = await WIKI.db.users.query().count('* as total').first()
this.set(toSafeInteger(total.total))
}
})
WIKI.logger.info('Metrics ready [ OK ]')
} else {
this.customMetrics = {}
register.clear()
}
return this
},
async render (res) {
try {
res.contentType(register.contentType)
res.send(await register.metrics())
} catch (err) {
res.status(500).end(err.message)
}
}
}

@ -1,309 +0,0 @@
import { DynamicThreadPool } from 'poolifier'
import os from 'node:os'
import fs from 'node:fs/promises'
import path from 'node:path'
import { CronExpressionParser } from 'cron-parser'
import { DateTime } from 'luxon'
import { v4 as uuid } from 'uuid'
import { createDeferred } from '../helpers/common.mjs'
import { camelCase, find, remove } from 'lodash-es'
export default {
workerPool: null,
maxWorkers: 1,
activeWorkers: 0,
pollingRef: null,
scheduledRef: null,
tasks: null,
completionPromises: [],
async init () {
this.maxWorkers = WIKI.config.scheduler.workers === 'auto' ? (os.cpus().length - 1) : WIKI.config.scheduler.workers
if (this.maxWorkers < 1) { this.maxWorkers = 1 }
WIKI.logger.info(`Initializing Worker Pool (Limit: ${this.maxWorkers})...`)
this.workerPool = new DynamicThreadPool(1, this.maxWorkers, path.join(WIKI.SERVERPATH, 'worker.mjs'), {
errorHandler: (err) => WIKI.logger.warn(err),
exitHandler: () => WIKI.logger.debug('A worker has gone offline.'),
onlineHandler: () => WIKI.logger.debug('New worker is online.')
})
this.tasks = {}
for (const f of (await fs.readdir(path.join(WIKI.SERVERPATH, 'tasks/simple')))) {
const taskName = camelCase(f.replace('.mjs', ''))
this.tasks[taskName] = (await import(path.join(WIKI.SERVERPATH, 'tasks/simple', f))).task
}
return this
},
async start () {
WIKI.logger.info('Starting Scheduler...')
// -> Add PostgreSQL Sub Channel
WIKI.db.listener.addChannel('scheduler', async payload => {
switch (payload.event) {
case 'newJob': {
if (this.activeWorkers < this.maxWorkers) {
this.activeWorkers++
await this.processJob()
this.activeWorkers--
}
break
}
case 'jobCompleted': {
const jobPromise = find(this.completionPromises, ['id', payload.id])
if (jobPromise) {
if (payload.state === 'success') {
jobPromise.resolve()
} else {
jobPromise.reject(new Error(payload.errorMessage))
}
setTimeout(() => {
remove(this.completionPromises, ['id', payload.id])
})
}
break
}
}
})
// -> Start scheduled jobs check
this.scheduledRef = setInterval(async () => {
this.addScheduled()
}, WIKI.config.scheduler.scheduledCheck * 1000)
// -> Add scheduled jobs on init
await this.addScheduled()
// -> Start job polling
this.pollingRef = setInterval(async () => {
this.processJob()
}, WIKI.config.scheduler.pollingCheck * 1000)
WIKI.logger.info('Scheduler: [ STARTED ]')
},
/**
* Add a job to the scheduler
* @param {Object} opts - Job options
* @param {string} opts.task - The task name to execute.
* @param {Object} [opts.payload={}] - An optional data object to pass to the job.
* @param {Date} [opts.waitUntil] - An optional datetime after which the task is allowed to run.
* @param {Number} [opts.maxRetries] - The number of times this job can be restarted upon failure. Uses server defaults if not provided.
* @param {Boolean} [opts.isScheduled=false] - Whether this is a scheduled job.
* @param {Boolean} [opts.notify=true] - Whether to notify all instances that a new job is available.
* @param {Boolean} [opts.promise=false] - Whether to return a promise property that resolves when the job completes.
* @returns {Promise}
*/
async addJob ({ task, payload = {}, waitUntil, maxRetries, isScheduled = false, notify = true, promise = false }) {
try {
const jobId = uuid()
const jobDefer = createDeferred()
if (promise) {
this.completionPromises.push({
id: jobId,
added: DateTime.utc(),
resolve: jobDefer.resolve,
reject: jobDefer.reject
})
}
await WIKI.db.knex('jobs')
.insert({
id: jobId,
task,
useWorker: !(typeof this.tasks[task] === 'function'),
payload,
maxRetries: maxRetries ?? WIKI.config.scheduler.maxRetries,
isScheduled,
waitUntil,
createdBy: WIKI.INSTANCE_ID
})
if (notify) {
WIKI.db.listener.publish('scheduler', {
source: WIKI.INSTANCE_ID,
event: 'newJob',
id: jobId
})
}
return {
id: jobId,
...promise && { promise: jobDefer.promise }
}
} catch (err) {
WIKI.logger.warn(`Failed to add job to scheduler: ${err.message}`)
}
},
async processJob () {
const jobIds = []
try {
const availableWorkers = this.maxWorkers - this.activeWorkers
if (availableWorkers < 1) {
WIKI.logger.debug('All workers are busy. Cannot process more jobs at the moment.')
return
}
await WIKI.db.knex.transaction(async trx => {
const jobs = await trx('jobs')
.whereIn('id', WIKI.db.knex.raw(`(SELECT id FROM jobs WHERE ("waitUntil" IS NULL OR "waitUntil" <= NOW()) ORDER BY id FOR UPDATE SKIP LOCKED LIMIT ${availableWorkers})`))
.returning('*')
.del()
if (jobs && jobs.length > 0) {
for (const job of jobs) {
WIKI.logger.info(`Processing new job ${job.id}: ${job.task}...`)
// -> Add to Job History
await WIKI.db.knex('jobHistory').insert({
id: job.id,
task: job.task,
state: 'active',
useWorker: job.useWorker,
wasScheduled: job.isScheduled,
payload: job.payload,
attempt: job.retries + 1,
maxRetries: job.maxRetries,
executedBy: WIKI.INSTANCE_ID,
createdAt: job.createdAt
}).onConflict('id').merge({
executedBy: WIKI.INSTANCE_ID,
startedAt: new Date()
})
jobIds.push(job.id)
// -> Start working on it
try {
if (job.useWorker) {
await this.workerPool.execute({
...job,
INSTANCE_ID: `${WIKI.INSTANCE_ID}:WKR`
})
} else {
await this.tasks[job.task](job.payload)
}
// -> Update job history (success)
await WIKI.db.knex('jobHistory').where({
id: job.id
}).update({
state: 'completed',
completedAt: new Date()
})
WIKI.logger.info(`Completed job ${job.id}: ${job.task}`)
WIKI.db.listener.publish('scheduler', {
source: WIKI.INSTANCE_ID,
event: 'jobCompleted',
state: 'success',
id: job.id
})
} catch (err) {
WIKI.logger.warn(`Failed to complete job ${job.id}: ${job.task} [ FAILED ]`)
WIKI.logger.warn(err)
// -> Update job history (fail)
await WIKI.db.knex('jobHistory').where({
id: job.id
}).update({
attempt: job.retries + 1,
state: 'failed',
lastErrorMessage: err.message
})
WIKI.db.listener.publish('scheduler', {
source: WIKI.INSTANCE_ID,
event: 'jobCompleted',
state: 'failed',
id: job.id,
errorMessage: err.message
})
// -> Reschedule for retry
if (job.retries < job.maxRetries) {
const backoffDelay = (2 ** job.retries) * WIKI.config.scheduler.retryBackoff
await trx('jobs').insert({
...job,
retries: job.retries + 1,
waitUntil: DateTime.utc().plus({ seconds: backoffDelay }).toJSDate(),
updatedAt: new Date()
})
WIKI.logger.warn(`Rescheduling new attempt for job ${job.id}: ${job.task}...`)
}
}
}
}
})
} catch (err) {
WIKI.logger.warn(err)
if (jobIds && jobIds.length > 0) {
WIKI.db.knex('jobHistory').whereIn('id', jobIds).update({
state: 'interrupted',
lastErrorMessage: err.message
})
}
}
},
async addScheduled () {
try {
await WIKI.db.knex.transaction(async trx => {
// -> Acquire lock
const jobLock = await trx('jobLock')
.where(
'key',
WIKI.db.knex('jobLock')
.select('key')
.where('key', 'cron')
.andWhere('lastCheckedAt', '<=', DateTime.utc().minus({ minutes: 5 }).toISO())
.forUpdate()
.skipLocked()
.limit(1)
).update({
lastCheckedBy: WIKI.INSTANCE_ID,
lastCheckedAt: DateTime.utc().toISO()
})
if (jobLock > 0) {
WIKI.logger.info('Scheduling future planned jobs...')
const scheduledJobs = await WIKI.db.knex('jobSchedule')
if (scheduledJobs?.length > 0) {
// -> Get existing scheduled jobs
const existingJobs = await WIKI.db.knex('jobs').where('isScheduled', true)
let totalAdded = 0
for (const job of scheduledJobs) {
// -> Get next planned iterations
const plannedIterations = CronExpressionParser.parse(job.cron, {
startDate: DateTime.utc().toJSDate(),
endDate: DateTime.utc().plus({ days: 1, minutes: 5 }).toJSDate(),
tz: 'UTC'
})
// -> Add a maximum of 10 future iterations for a single task
let addedFutureJobs = 0
while (true) {
try {
const next = plannedIterations.next()
// -> Ensure this iteration isn't already scheduled
if (!existingJobs.some(j => j.task === job.task && j.waitUntil.getTime() === next.value.getTime())) {
this.addJob({
task: job.task,
useWorker: !(typeof this.tasks[job.task] === 'function'),
payload: job.payload,
isScheduled: true,
waitUntil: next.value.toISOString(),
notify: false
})
addedFutureJobs++
totalAdded++
}
// -> No more iterations for this period or max iterations count reached
if (next.done || addedFutureJobs >= 10) { break }
} catch (err) {
break
}
}
}
if (totalAdded > 0) {
WIKI.logger.info(`Scheduled ${totalAdded} new future planned jobs: [ OK ]`)
} else {
WIKI.logger.info('No new future planned jobs to schedule: [ OK ]')
}
}
}
})
} catch (err) {
WIKI.logger.warn(err)
}
},
async stop () {
WIKI.logger.info('Stopping Scheduler...')
clearInterval(this.scheduledRef)
clearInterval(this.pollingRef)
await this.workerPool.destroy()
WIKI.logger.info('Scheduler: [ STOPPED ]')
}
}

@ -1,239 +0,0 @@
import fs from 'node:fs/promises'
import http from 'node:http'
import https from 'node:https'
import { ApolloServer } from '@apollo/server'
import { expressMiddleware } from '@as-integrations/express5'
import { isEmpty } from 'lodash-es'
import { Server as IoServer } from 'socket.io'
import { ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageProductionDefault } from '@apollo/server/plugin/landingPage/default'
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'
import { initSchema } from '../graph/index.mjs'
export default {
graph: null,
http: null,
https: null,
ws: null,
connections: new Map(),
le: null,
/**
* Initialize HTTP Server
*/
async initHTTP () {
WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
this.http = http.createServer(WIKI.app)
this.http.on('error', (error) => {
if (error.syscall !== 'listen') {
throw error
}
switch (error.code) {
case 'EACCES':
WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
return process.exit(1)
case 'EADDRINUSE':
WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')
return process.exit(1)
default:
throw error
}
})
this.http.on('listening', () => {
WIKI.logger.info('HTTP Server: [ RUNNING ]')
})
this.http.on('connection', conn => {
const connKey = `http:${conn.remoteAddress}:${conn.remotePort}`
this.connections.set(connKey, conn)
conn.on('close', () => {
this.connections.delete(connKey)
})
})
},
/**
* Start HTTP Server
*/
async startHTTP () {
this.http.listen(WIKI.config.port, WIKI.config.bindIP)
},
/**
* Initialize HTTPS Server
*/
async initHTTPS () {
if (WIKI.config.ssl.provider === 'letsencrypt') {
this.le = require('./letsencrypt')
await this.le.init()
}
WIKI.logger.info(`HTTPS Server on port: [ ${WIKI.config.ssl.port} ]`)
const tlsOpts = {}
try {
if (WIKI.config.ssl.format === 'pem') {
tlsOpts.key = WIKI.config.ssl.inline ? WIKI.config.ssl.key : await fs.readFile(WIKI.config.ssl.key, 'utf-8')
tlsOpts.cert = WIKI.config.ssl.inline ? WIKI.config.ssl.cert : await fs.readFile(WIKI.config.ssl.cert, 'utf-8')
} else {
tlsOpts.pfx = WIKI.config.ssl.inline ? WIKI.config.ssl.pfx : await fs.readFile(WIKI.config.ssl.pfx, 'utf-8')
}
if (!isEmpty(WIKI.config.ssl.passphrase)) {
tlsOpts.passphrase = WIKI.config.ssl.passphrase
}
if (!isEmpty(WIKI.config.ssl.dhparam)) {
tlsOpts.dhparam = WIKI.config.ssl.dhparam
}
} catch (err) {
WIKI.logger.error('Failed to setup HTTPS server parameters:')
WIKI.logger.error(err)
return process.exit(1)
}
this.https = https.createServer(tlsOpts, WIKI.app)
this.https.listen(WIKI.config.ssl.port, WIKI.config.bindIP)
this.https.on('error', (error) => {
if (error.syscall !== 'listen') {
throw error
}
switch (error.code) {
case 'EACCES':
WIKI.logger.error('Listening on port ' + WIKI.config.ssl.port + ' requires elevated privileges!')
return process.exit(1)
case 'EADDRINUSE':
WIKI.logger.error('Port ' + WIKI.config.ssl.port + ' is already in use!')
return process.exit(1)
default:
throw error
}
})
this.https.on('listening', () => {
WIKI.logger.info('HTTPS Server: [ RUNNING ]')
})
this.https.on('connection', conn => {
const connKey = `https:${conn.remoteAddress}:${conn.remotePort}`
this.connections.set(connKey, conn)
conn.on('close', () => {
this.connections.delete(connKey)
})
})
},
/**
* Start HTTPS Server
*/
async startHTTPS () {
this.https.listen(WIKI.config.ssl.port, WIKI.config.bindIP)
},
/**
* Start GraphQL Server
*/
async startGraphQL () {
const graphqlSchema = await initSchema()
this.graph = new ApolloServer({
schema: graphqlSchema,
allowBatchedHttpRequests: true,
csrfPrevention: true,
cache: 'bounded',
plugins: [
process.env.NODE_ENV === 'production'
? ApolloServerPluginLandingPageProductionDefault({
footer: false
})
: ApolloServerPluginLandingPageLocalDefault({
footer: false,
embed: {
endpointIsEditable: false,
runTelemetry: false
}
})
// ApolloServerPluginDrainHttpServer({ httpServer: this.http })
// ...(this.https && ApolloServerPluginDrainHttpServer({ httpServer: this.https }))
]
})
await this.graph.start()
WIKI.app.use(graphqlUploadExpress({
maxFileSize: WIKI.config.security.uploadMaxFileSize,
maxFiles: WIKI.config.security.uploadMaxFiles
}))
WIKI.app.use('/_graphql', expressMiddleware(this.graph, {
context: ({ req, res }) => ({ req, res })
}))
},
/**
* Start Socket.io WebSocket Server
*/
async initWebSocket () {
if (this.https) {
this.ws = new IoServer(this.https, {
path: '/_ws/',
serveClient: false
})
WIKI.logger.info('WebSocket Server attached to HTTPS Server [ OK ]')
} else {
this.ws = new IoServer(this.http, {
path: '/_ws/',
serveClient: false,
cors: true // TODO: dev only, replace with app settings once stable
})
WIKI.logger.info('WebSocket Server attached to HTTP Server [ OK ]')
}
},
/**
* Close all active connections
*/
closeConnections (mode = 'all') {
for (const [key, conn] of this.connections) {
if (mode !== 'all' && key.indexOf(`${mode}:`) !== 0) {
continue
}
conn.destroy()
this.connections.delete(key)
}
if (mode === 'all') {
this.connections.clear()
}
},
/**
* Stop all servers
*/
async stopServers () {
this.closeConnections()
if (this.http) {
await new Promise(resolve => this.http.close(resolve))
this.http = null
}
if (this.https) {
await new Promise(resolve => this.https.close(resolve))
this.https = null
}
this.graph = null
},
/**
* Restart Server
*/
async restartServer (srv = 'https') {
this.closeConnections(srv)
switch (srv) {
case 'http':
if (this.http) {
await new Promise(resolve => this.http.close(resolve))
this.http = null
}
this.initHTTP()
this.startHTTP()
break
case 'https':
if (this.https) {
await new Promise(resolve => this.https.close(resolve))
this.https = null
}
this.initHTTPS()
this.startHTTPS()
break
default:
throw new Error('Cannot restart server: Invalid designation')
}
}
}

@ -1,74 +0,0 @@
const fs = require('fs-extra')
const path = require('path')
const _ = require('lodash')
module.exports = {
async init () {
if (!WIKI.config.offline) {
return
}
const sideloadExists = await fs.pathExists(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'sideload'))
if (!sideloadExists) {
return
}
WIKI.logger.info('Sideload directory detected. Looking for packages...')
try {
await this.importLocales()
} catch (err) {
WIKI.logger.warn(err)
}
},
async importLocales() {
const localeExists = await fs.pathExists(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'sideload/locales.json'))
if (localeExists) {
WIKI.logger.info('Found locales master file. Importing locale packages...')
let importedLocales = 0
const locales = await fs.readJson(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'sideload/locales.json'))
if (locales && _.has(locales, 'data.localization.locales')) {
for (const locale of locales.data.localization.locales) {
try {
const localeData = await fs.readJson(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `sideload/${locale.code}.json`))
if (localeData) {
WIKI.logger.info(`Importing ${locale.name} locale package...`)
let lcObj = {}
_.forOwn(localeData, (value, key) => {
if (_.includes(key, '::')) { return }
if (_.isEmpty(value)) { value = key }
_.set(lcObj, key.replace(':', '.'), value)
})
const localeDbExists = await WIKI.db.locales.query().select('code').where('code', locale.code).first()
if (localeDbExists) {
await WIKI.db.locales.query().update({
code: locale.code,
strings: lcObj,
isRTL: locale.isRTL,
name: locale.name,
nativeName: locale.nativeName
}).where('code', locale.code)
} else {
await WIKI.db.locales.query().insert({
code: locale.code,
strings: lcObj,
isRTL: locale.isRTL,
name: locale.name,
nativeName: locale.nativeName
})
}
importedLocales++
}
} catch (err) {
// skip
}
}
WIKI.logger.info(`Imported ${importedLocales} locale packages: [COMPLETED]`)
}
}
}
}

@ -1,21 +0,0 @@
import fse from 'fs-extra'
import path from 'node:path'
export default {
updates: {
channel: 'BETA',
version: WIKI.version,
releaseDate: WIKI.releaseDate,
minimumVersionRequired: '3.0.0-beta.0',
minimumNodeRequired: '18.0.0'
},
init () {
fse.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets'))
fse.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
// Clear content cache
fse.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
return this
}
}

@ -1,33 +0,0 @@
const _ = require('lodash')
module.exports = {
async migrate (knex) {
const migrationsTableExists = await knex.schema.hasTable('migrations')
if (!migrationsTableExists) {
return
}
const migrations = await knex('migrations')
if (_.some(migrations, m => m.name.indexOf('2.0.0') === 0)) {
WIKI.logger.info('Found legacy 2.x installation of Wiki.js...')
if (_.some(migrations, m => m.name.indexOf('2.5.128') === 0)) {
// TODO: 2.x MIGRATIONS for 3.0
WIKI.logger.error('Upgrading from 2.x is not yet supported. A future release will allow for upgrade from 2.x. Exiting...')
process.exit(1)
// -> Cleanup migration table
await knex('migrations').truncate()
// -> Advance to stable 3.0 migration state
await knex('migrations').insert({
name: '3.0.0.js',
batch: 1,
migration_time: knex.fn.now()
})
} else {
console.error('CANNOT UPGRADE FROM OLDER UNSUPPORTED VERSION. UPGRADE TO THE LATEST 2.X VERSION FIRST! Exiting...')
process.exit(1)
}
}
}
}

@ -1,930 +0,0 @@
import { v4 as uuid } from 'uuid'
import bcrypt from 'bcryptjs'
import crypto from 'node:crypto'
import { DateTime } from 'luxon'
import { pem2jwk } from 'pem-jwk'
export async function up (knex) {
WIKI.logger.info('Running 3.0.0 database migration...')
// =====================================
// PG EXTENSIONS
// =====================================
await knex.raw('CREATE EXTENSION IF NOT EXISTS ltree;')
await knex.raw('CREATE EXTENSION IF NOT EXISTS pgcrypto;')
await knex.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm;')
await knex.schema
// =====================================
// MODEL TABLES
// =====================================
// ACTIVITY LOGS -----------------------
.createTable('activityLogs', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.timestamp('ts').notNullable().defaultTo(knex.fn.now())
table.string('action').notNullable()
table.jsonb('meta').notNullable()
})
// ANALYTICS ---------------------------
.createTable('analytics', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('module').notNullable()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.jsonb('config').notNullable()
})
// API KEYS ----------------------------
.createTable('apiKeys', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('name').notNullable()
table.text('key').notNullable()
table.timestamp('expiration').notNullable().defaultTo(knex.fn.now())
table.boolean('isRevoked').notNullable().defaultTo(false)
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// ASSETS ------------------------------
.createTable('assets', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('fileName').notNullable()
table.string('fileExt').notNullable()
table.boolean('isSystem').notNullable().defaultTo(false)
table.enum('kind', ['document', 'image', 'other']).notNullable().defaultTo('other')
table.string('mimeType').notNullable().defaultTo('application/octet-stream')
table.integer('fileSize').unsigned().comment('In bytes')
table.jsonb('meta').notNullable().defaultTo('{}')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
table.binary('data')
table.binary('preview')
table.enum('previewState', ['none', 'pending', 'ready', 'failed']).notNullable().defaultTo('none')
table.jsonb('storageInfo')
})
// AUTHENTICATION ----------------------
.createTable('authentication', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('module').notNullable()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.string('displayName').notNullable().defaultTo('')
table.jsonb('config').notNullable().defaultTo('{}')
table.boolean('registration').notNullable().defaultTo(false)
table.string('allowedEmailRegex').notNullable().defaultTo('')
table.specificType('autoEnrollGroups', 'uuid[]')
})
.createTable('blocks', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('block').notNullable()
table.string('name').notNullable()
table.string('description').notNullable()
table.string('icon')
table.boolean('isEnabled').notNullable().defaultTo(false)
table.boolean('isCustom').notNullable().defaultTo(false)
table.json('config').notNullable()
})
// COMMENT PROVIDERS -------------------
.createTable('commentProviders', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('module').notNullable()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.json('config').notNullable()
})
// COMMENTS ----------------------------
.createTable('comments', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.uuid('replyTo')
table.text('content').notNullable()
table.text('render').notNullable().defaultTo('')
table.string('name').notNullable().defaultTo('')
table.string('email').notNullable().defaultTo('')
table.string('ip').notNullable().defaultTo('')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// GROUPS ------------------------------
.createTable('groups', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('name').notNullable()
table.jsonb('permissions').notNullable()
table.jsonb('rules').notNullable()
table.string('redirectOnLogin').notNullable().defaultTo('')
table.string('redirectOnFirstLogin').notNullable().defaultTo('')
table.string('redirectOnLogout').notNullable().defaultTo('')
table.boolean('isSystem').notNullable().defaultTo(false)
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// HOOKS -------------------------------
.createTable('hooks', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('name').notNullable()
table.jsonb('events').notNullable().defaultTo('[]')
table.string('url').notNullable()
table.boolean('includeMetadata').notNullable().defaultTo(false)
table.boolean('includeContent').notNullable().defaultTo(false)
table.boolean('acceptUntrusted').notNullable().defaultTo(false)
table.string('authHeader')
table.enum('state', ['pending', 'error', 'success']).notNullable().defaultTo('pending')
table.string('lastErrorMessage')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// JOB HISTORY -------------------------
.createTable('jobHistory', table => {
table.uuid('id').notNullable().primary()
table.string('task').notNullable()
table.enum('state', ['active', 'completed', 'failed', 'interrupted']).notNullable()
table.boolean('useWorker').notNullable().defaultTo(false)
table.boolean('wasScheduled').notNullable().defaultTo(false)
table.jsonb('payload')
table.integer('attempt').notNullable().defaultTo(1)
table.integer('maxRetries').notNullable().defaultTo(0)
table.text('lastErrorMessage')
table.string('executedBy')
table.timestamp('createdAt').notNullable()
table.timestamp('startedAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('completedAt')
})
// JOB SCHEDULE ------------------------
.createTable('jobSchedule', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('task').notNullable()
table.string('cron').notNullable()
table.string('type').notNullable().defaultTo('system')
table.jsonb('payload')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// JOB SCHEDULE ------------------------
.createTable('jobLock', table => {
table.string('key').notNullable().primary()
table.string('lastCheckedBy')
table.timestamp('lastCheckedAt').notNullable().defaultTo(knex.fn.now())
})
// JOBS --------------------------------
.createTable('jobs', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('task').notNullable()
table.boolean('useWorker').notNullable().defaultTo(false)
table.jsonb('payload')
table.integer('retries').notNullable().defaultTo(0)
table.integer('maxRetries').notNullable().defaultTo(0)
table.timestamp('waitUntil')
table.boolean('isScheduled').notNullable().defaultTo(false)
table.string('createdBy')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// LOCALES -----------------------------
.createTable('locales', table => {
table.string('code', 10).notNullable().primary()
table.string('name').notNullable()
table.string('nativeName').notNullable()
table.string('language', 2).notNullable().index()
table.string('region', 2)
table.string('script', 4)
table.boolean('isRTL').notNullable().defaultTo(false)
table.jsonb('strings')
table.integer('completeness').notNullable().defaultTo(0)
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// NAVIGATION ----------------------------
.createTable('navigation', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.jsonb('items').notNullable().defaultTo('[]')
})
// PAGE HISTORY ------------------------
.createTable('pageHistory', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.uuid('pageId').notNullable().index()
table.string('action').defaultTo('updated')
table.string('reason')
table.jsonb('affectedFields').notNullable().defaultTo('[]')
table.string('locale', 10).notNullable().defaultTo('en')
table.string('path').notNullable()
table.string('hash').notNullable()
table.string('alias')
table.string('title').notNullable()
table.string('description')
table.string('icon')
table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft')
table.timestamp('publishStartDate')
table.timestamp('publishEndDate')
table.jsonb('config').notNullable().defaultTo('{}')
table.jsonb('relations').notNullable().defaultTo('[]')
table.text('content')
table.text('render')
table.jsonb('toc')
table.string('editor').notNullable()
table.string('contentType').notNullable()
table.jsonb('scripts').notNullable().defaultTo('{}')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('versionDate').notNullable().defaultTo(knex.fn.now())
})
// PAGE LINKS --------------------------
.createTable('pageLinks', table => {
table.increments('id').primary()
table.string('path').notNullable()
table.string('locale', 10).notNullable()
})
// PAGES -------------------------------
.createTable('pages', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('locale', 10).notNullable()
table.string('path').notNullable()
table.string('hash').notNullable()
table.string('alias')
table.string('title').notNullable()
table.string('description')
table.string('icon')
table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft')
table.timestamp('publishStartDate')
table.timestamp('publishEndDate')
table.jsonb('config').notNullable().defaultTo('{}')
table.jsonb('relations').notNullable().defaultTo('[]')
table.text('content')
table.text('render')
table.text('searchContent')
table.specificType('ts', 'tsvector').index('pages_ts_idx', { indexType: 'GIN' })
table.specificType('tags', 'text[]').index('pages_tags_idx', { indexType: 'GIN' })
table.jsonb('toc')
table.string('editor').notNullable()
table.string('contentType').notNullable()
table.boolean('isBrowsable').notNullable().defaultTo(true)
table.boolean('isSearchable').notNullable().defaultTo(true)
table.specificType('isSearchableComputed', 'boolean GENERATED ALWAYS AS ("publishState" != \'draft\' AND "isSearchable") STORED').index()
table.string('password')
table.integer('ratingScore').notNullable().defaultTo(0)
table.integer('ratingCount').notNullable().defaultTo(0)
table.jsonb('scripts').notNullable().defaultTo('{}')
table.jsonb('historyData').notNullable().defaultTo('{}')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// RENDERERS ---------------------------
.createTable('renderers', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('module').notNullable()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.jsonb('config').notNullable().defaultTo('{}')
})
// SETTINGS ----------------------------
.createTable('settings', table => {
table.string('key').notNullable().primary()
table.jsonb('value')
})
// SITES -------------------------------
.createTable('sites', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('hostname').notNullable()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.jsonb('config').notNullable()
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
})
// STORAGE -----------------------------
.createTable('storage', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('module').notNullable()
table.boolean('isEnabled').notNullable().defaultTo(false)
table.jsonb('contentTypes')
table.jsonb('assetDelivery')
table.jsonb('versioning')
table.jsonb('schedule')
table.jsonb('config')
table.jsonb('state')
})
// TAGS --------------------------------
.createTable('tags', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('tag').notNullable()
table.integer('usageCount').notNullable().defaultTo(0)
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// TREE --------------------------------
.createTable('tree', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.specificType('folderPath', 'ltree').index().index('tree_folderpath_gist_idx', { indexType: 'GIST' })
table.string('fileName').notNullable().index()
table.string('hash').notNullable().index()
table.enu('type', ['folder', 'page', 'asset']).notNullable().index()
table.string('locale', 10).notNullable().defaultTo('en').index()
table.string('title').notNullable()
table.enum('navigationMode', ['inherit', 'override', 'overrideExact', 'hide', 'hideExact']).notNullable().defaultTo('inherit').index()
table.uuid('navigationId').index()
table.specificType('tags', 'text[]').index('tree_tags_idx', { indexType: 'GIN' })
table.jsonb('meta').notNullable().defaultTo('{}')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// USER AVATARS ------------------------
.createTable('userAvatars', table => {
table.uuid('id').notNullable().primary()
table.binary('data').notNullable()
})
// USER EDITOR SETTINGS ----------------
.createTable('userEditorSettings', table => {
table.uuid('id').notNullable()
table.string('editor').notNullable()
table.jsonb('config').notNullable().defaultTo('{}')
table.primary(['id', 'editor'])
})
// USER KEYS ---------------------------
.createTable('userKeys', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('kind').notNullable()
table.string('token').notNullable()
table.jsonb('meta').notNullable().defaultTo('{}')
table.timestamp('validUntil').notNullable()
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
})
// USERS -------------------------------
.createTable('users', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('email').notNullable()
table.string('name').notNullable()
table.jsonb('auth').notNullable().defaultTo('{}')
table.jsonb('meta').notNullable().defaultTo('{}')
table.jsonb('passkeys').notNullable().defaultTo('{}')
table.jsonb('prefs').notNullable().defaultTo('{}')
table.boolean('hasAvatar').notNullable().defaultTo(false)
table.boolean('isSystem').notNullable().defaultTo(false)
table.boolean('isActive').notNullable().defaultTo(false)
table.boolean('isVerified').notNullable().defaultTo(false)
table.timestamp('lastLoginAt').index()
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
})
// =====================================
// RELATION TABLES
// =====================================
// USER GROUPS -------------------------
.createTable('userGroups', table => {
table.increments('id').primary()
table.uuid('userId').references('id').inTable('users').onDelete('CASCADE')
table.uuid('groupId').references('id').inTable('groups').onDelete('CASCADE')
})
// =====================================
// REFERENCES
// =====================================
.table('activityLogs', table => {
table.uuid('userId').notNullable().references('id').inTable('users')
})
.table('analytics', table => {
table.uuid('siteId').notNullable().references('id').inTable('sites')
})
.table('assets', table => {
table.uuid('authorId').notNullable().references('id').inTable('users')
table.uuid('siteId').notNullable().references('id').inTable('sites').index()
})
.table('blocks', table => {
table.uuid('siteId').notNullable().references('id').inTable('sites')
})
.table('commentProviders', table => {
table.uuid('siteId').notNullable().references('id').inTable('sites')
})
.table('comments', table => {
table.uuid('pageId').notNullable().references('id').inTable('pages').index()
table.uuid('authorId').notNullable().references('id').inTable('users').index()
})
.table('navigation', table => {
table.uuid('siteId').notNullable().references('id').inTable('sites').index()
})
.table('pageHistory', table => {
table.uuid('authorId').notNullable().references('id').inTable('users')
table.uuid('siteId').notNullable().references('id').inTable('sites').index()
})
.table('pageLinks', table => {
table.uuid('pageId').notNullable().references('id').inTable('pages').onDelete('CASCADE')
table.index(['path', 'locale'])
})
.table('pages', table => {
table.uuid('authorId').notNullable().references('id').inTable('users').index()
table.uuid('creatorId').notNullable().references('id').inTable('users').index()
table.uuid('ownerId').notNullable().references('id').inTable('users').index()
table.uuid('siteId').notNullable().references('id').inTable('sites').index()
})
.table('storage', table => {
table.uuid('siteId').notNullable().references('id').inTable('sites')
})
.table('tags', table => {
table.uuid('siteId').notNullable().references('id').inTable('sites')
table.unique(['siteId', 'tag'])
})
.table('tree', table => {
table.uuid('siteId').notNullable().references('id').inTable('sites')
})
.table('userKeys', table => {
table.uuid('userId').notNullable().references('id').inTable('users')
})
// =====================================
// TS WORD SUGGESTION TABLE
// =====================================
.createTable('autocomplete', table => {
table.text('word')
})
.raw('CREATE INDEX "autocomplete_idx" ON "autocomplete" USING GIN (word gin_trgm_ops)')
// =====================================
// DEFAULT DATA
// =====================================
// -> GENERATE IDS
const groupAdminId = uuid()
const groupUserId = WIKI.data.systemIds.usersGroupId
const groupGuestId = WIKI.data.systemIds.guestsGroupId
const siteId = uuid()
const authModuleId = WIKI.data.systemIds.localAuthId
const userAdminId = uuid()
const userGuestId = uuid()
// -> SYSTEM CONFIG
WIKI.logger.info('Generating certificates...')
const secret = crypto.randomBytes(32).toString('hex')
const certs = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: secret
}
})
await knex('settings').insert([
{
key: 'api',
value: {
isEnabled: false
}
},
{
key: 'auth',
value: {
audience: 'urn:wiki.js',
tokenExpiration: '30m',
tokenRenewal: '14d',
certs: {
jwk: pem2jwk(certs.publicKey),
public: certs.publicKey,
private: certs.privateKey
},
secret,
rootAdminGroupId: groupAdminId,
rootAdminUserId: userAdminId,
guestUserId: userGuestId
}
},
{
key: 'flags',
value: {
experimental: false,
authDebug: false,
sqlLog: false
}
},
{
key: 'icons',
value: {
fa: {
isActive: true,
config: {
version: 6,
license: 'free',
token: ''
}
},
la: {
isActive: true
}
}
},
{
key: 'mail',
value: {
senderName: '',
senderEmail: '',
defaultBaseURL: 'https://wiki.example.com',
host: '',
port: 465,
name: '',
secure: true,
verifySSL: true,
user: '',
pass: '',
useDKIM: false,
dkimDomainName: '',
dkimKeySelector: '',
dkimPrivateKey: ''
}
},
{
key: 'metrics',
value: {
isEnabled: false
}
},
{
key: 'search',
value: {
termHighlighting: true,
dictOverrides: {}
}
},
{
key: 'security',
value: {
corsConfig: '',
corsMode: 'OFF',
cspDirectives: '',
disallowFloc: true,
disallowIframe: true,
disallowOpenRedirect: true,
enforceCsp: false,
enforceHsts: false,
enforceSameOriginReferrerPolicy: true,
forceAssetDownload: true,
hstsDuration: 0,
trustProxy: false,
authJwtAudience: 'urn:wiki.js',
authJwtExpiration: '30m',
authJwtRenewablePeriod: '14d',
uploadMaxFileSize: 10485760,
uploadMaxFiles: 20,
uploadScanSVG: true
}
},
{
key: 'update',
value: {
lastCheckedAt: null,
version: WIKI.version,
versionDate: WIKI.releaseDate
}
},
{
key: 'userDefaults',
value: {
timezone: 'America/New_York',
dateFormat: 'YYYY-MM-DD',
timeFormat: '12h'
}
}
])
// -> DEFAULT SITE
await knex('sites').insert({
id: siteId,
hostname: '*',
isEnabled: true,
config: {
title: 'Default Site',
description: '',
company: '',
contentLicense: '',
footerExtra: '',
pageExtensions: ['md', 'html', 'txt'],
pageCasing: true,
discoverable: false,
defaults: {
tocDepth: {
min: 1,
max: 2
}
},
features: {
browse: true,
ratings: false,
ratingsMode: 'off',
comments: false,
contributions: false,
profile: true,
reasonForChange: 'required',
search: true
},
logoText: true,
sitemap: true,
robots: {
index: true,
follow: true
},
authStrategies: [{ id: authModuleId, order: 0, isVisible: true }],
locales: {
primary: 'en',
active: ['en']
},
assets: {
logo: false,
logoExt: 'svg',
favicon: false,
faviconExt: 'svg',
loginBg: false
},
editors: {
asciidoc: {
isActive: true,
config: {}
},
markdown: {
isActive: true,
config: {
allowHTML: true,
kroki: false,
krokiServerUrl: 'https://kroki.io',
latexEngine: 'katex',
lineBreaks: true,
linkify: true,
multimdTable: true,
plantuml: false,
plantumlServerUrl: 'https://www.plantuml.com/plantuml/',
quotes: 'english',
tabWidth: 2,
typographer: false,
underline: true
}
},
wysiwyg: {
isActive: true,
config: {}
}
},
theme: {
dark: false,
codeBlocksTheme: 'github-dark',
colorPrimary: '#1976D2',
colorSecondary: '#02C39A',
colorAccent: '#FF9800',
colorHeader: '#000000',
colorSidebar: '#1976D2',
injectCSS: '',
injectHead: '',
injectBody: '',
contentWidth: 'full',
sidebarPosition: 'left',
tocPosition: 'right',
showSharingMenu: true,
showPrintBtn: true,
baseFont: 'roboto',
contentFont: 'roboto'
},
uploads: {
conflictBehavior: 'overwrite',
normalizeFilename: true
}
}
})
// -> DEFAULT GROUPS
await knex('groups').insert([
{
id: groupAdminId,
name: 'Administrators',
permissions: JSON.stringify(['manage:system']),
rules: JSON.stringify([]),
isSystem: true
},
{
id: groupUserId,
name: 'Users',
permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
rules: JSON.stringify([
{
id: uuid(),
name: 'Default Rule',
roles: ['read:pages', 'read:assets', 'read:comments'],
match: 'START',
mode: 'ALLOW',
path: '',
locales: [],
sites: []
}
]),
isSystem: true
},
{
id: groupGuestId,
name: 'Guests',
permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
rules: JSON.stringify([
{
id: uuid(),
name: 'Default Rule',
roles: ['read:pages', 'read:assets', 'read:comments'],
match: 'START',
mode: 'DENY',
path: '',
locales: [],
sites: []
}
]),
isSystem: true
}
])
// -> AUTHENTICATION MODULE
await knex('authentication').insert({
id: authModuleId,
module: 'local',
isEnabled: true,
displayName: 'Local Authentication',
config: JSON.stringify({
emailValidation: true,
enforceTfa: false
})
})
// -> USERS
await knex('users').insert([
{
id: userAdminId,
email: process.env.ADMIN_EMAIL ?? 'admin@example.com',
auth: {
[authModuleId]: {
password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12),
mustChangePwd: !process.env.ADMIN_PASS,
restrictLogin: false,
tfaIsActive: false,
tfaRequired: false,
tfaSecret: ''
}
},
name: 'Administrator',
isSystem: false,
isActive: true,
isVerified: true,
meta: {
location: '',
jobTitle: '',
pronouns: ''
},
prefs: {
timezone: 'America/New_York',
dateFormat: 'YYYY-MM-DD',
timeFormat: '12h',
appearance: 'site',
cvd: 'none'
}
},
{
id: userGuestId,
email: 'guest@example.com',
auth: {},
name: 'Guest',
isSystem: true,
isActive: true,
isVerified: true,
meta: {},
prefs: {
timezone: 'America/New_York',
dateFormat: 'YYYY-MM-DD',
timeFormat: '12h',
appearance: 'site',
cvd: 'none'
}
}
])
await knex('userGroups').insert([
{
userId: userAdminId,
groupId: groupAdminId
},
{
userId: userAdminId,
groupId: groupUserId
},
{
userId: userGuestId,
groupId: groupGuestId
}
])
// -> BLOCKS
await knex('blocks').insert({
block: 'index',
name: 'Index',
description: 'Show a list of pages matching a path or set of tags.',
icon: 'rules',
isCustom: false,
isEnabled: true,
config: {},
siteId
})
// -> NAVIGATION
await knex('navigation').insert({
id: siteId,
items: JSON.stringify([
{
id: uuid(),
type: 'header',
label: 'Sample Header',
visibilityGroups: []
},
{
id: uuid(),
type: 'link',
icon: 'mdi-file-document-outline',
label: 'Sample Link 1',
target: '/',
openInNewWindow: false,
visibilityGroups: [],
children: []
},
{
id: uuid(),
type: 'link',
icon: 'mdi-book-open-variant',
label: 'Sample Link 2',
target: '/',
openInNewWindow: false,
visibilityGroups: [],
children: []
},
{
id: uuid(),
type: 'separator',
visibilityGroups: []
},
{
id: uuid(),
type: 'link',
icon: 'mdi-airballoon',
label: 'Sample Link 3',
target: '/',
openInNewWindow: false,
visibilityGroups: [],
children: []
}
]),
siteId
})
// -> STORAGE MODULE
await knex('storage').insert({
module: 'db',
siteId,
isEnabled: true,
contentTypes: {
activeTypes: ['pages', 'images', 'documents', 'others', 'large'],
largeThreshold: '5MB'
},
assetDelivery: {
streaming: true,
directAccess: false
},
versioning: {
enabled: false
},
state: {
current: 'ok'
}
})
// -> SCHEDULED JOBS
await knex('jobSchedule').insert([
{
task: 'checkVersion',
cron: '0 0 * * *',
type: 'system'
},
{
task: 'cleanJobHistory',
cron: '5 0 * * *',
type: 'system'
},
// {
// task: 'refreshAutocomplete',
// cron: '0 */6 * * *',
// type: 'system'
// },
{
task: 'updateLocales',
cron: '0 0 * * *',
type: 'system'
}
])
await knex('jobLock').insert({
key: 'cron',
lastCheckedBy: 'init',
lastCheckedAt: DateTime.utc().minus({ hours: 1 }).toISO()
})
WIKI.logger.info('Completed 3.0.0 database migration.')
}
export function down (knex) { }

@ -1,26 +0,0 @@
import path from 'node:path'
import fse from 'fs-extra'
import semver from 'semver'
export default {
/**
* Gets the migration names
* @returns Promise<string[]>
*/
async getMigrations() {
const baseMigrationPath = path.join(WIKI.SERVERPATH, 'db/migrations')
const migrationFiles = await fse.readdir(baseMigrationPath)
return migrationFiles.map(m => m.replace('.mjs', '')).sort(semver.compare).map(m => ({
file: m,
directory: baseMigrationPath
}))
},
getMigrationName(migration) {
return migration.file.indexOf('.mjs') >= 0 ? migration.file : `${migration.file}.mjs`
},
async getMigration(migration) {
return import(path.join(WIKI.SERVERPATH, 'db/migrations', `${migration.file}.mjs`))
}
}

@ -1,19 +0,0 @@
import neostandard from 'neostandard'
export default neostandard({
globals: {
document: false,
navigator: false,
window: false,
WIKI: true
},
ignores: [
'**/node_modules/**',
'**/*.min.js',
'coverage/**',
'repo/**',
'data/**',
'logs/**',
'locales/**'
]
})

@ -1,61 +0,0 @@
import { merge } from 'lodash-es'
import fs from 'node:fs/promises'
import path from 'node:path'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { defaultKeyGenerator, rateLimitDirective } from 'graphql-rate-limit-directive'
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'
import DateScalar from './scalars/date.mjs'
import JSONScalar from './scalars/json.mjs'
import UUIDScalar from './scalars/uuid.mjs'
export async function initSchema () {
// Rate Limiter
const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective({
keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${defaultKeyGenerator(directiveArgs, source, args, context, info)}`
})
// Schemas
WIKI.logger.info(`Loading GraphQL Schema...`)
const typeDefs = [
rateLimitDirectiveTypeDefs
]
const schemaList = await fs.readdir(path.join(WIKI.SERVERPATH, 'graph/schemas'))
for (const schemaFile of schemaList) {
typeDefs.push(await fs.readFile(path.join(WIKI.SERVERPATH, `graph/schemas/${schemaFile}`), 'utf8'))
}
// Resolvers
WIKI.logger.info(`Loading GraphQL Resolvers...`)
let resolvers = {
Date: DateScalar,
JSON: JSONScalar,
UUID: UUIDScalar,
Upload: GraphQLUpload
}
const resolverList = await fs.readdir(path.join(WIKI.SERVERPATH, 'graph/resolvers'))
for (const resolverFile of resolverList) {
const resolver = (await import(path.join(WIKI.SERVERPATH, 'graph/resolvers', resolverFile))).default
merge(resolvers, resolver)
}
// Make executable schema
WIKI.logger.info(`Compiling GraphQL Schema...`)
let schema = makeExecutableSchema({
typeDefs,
resolvers
})
// Apply schema transforms
schema = rateLimitDirectiveTransformer(schema)
WIKI.logger.info(`GraphQL Schema: [ OK ]`)
return schema
}

@ -1,57 +0,0 @@
import { find, get, reduce, set, sortBy, transform } from 'lodash-es'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
export default {
Query: {
async analyticsProviders (obj, args, context, info) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
let providers = await WIKI.db.analytics.getProviders(args.isEnabled)
providers = providers.map(stg => {
const providerInfo = find(WIKI.data.analytics, ['key', stg.key]) || {}
return {
...providerInfo,
...stg,
config: sortBy(transform(stg.config, (res, value, key) => {
const configData = get(providerInfo.props, key, {})
res.push({
key,
value: JSON.stringify({
...configData,
value
})
})
}, []), 'key')
}
})
return providers
}
},
Mutation: {
async updateAnalyticsProviders (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
for (const str of args.providers) {
await WIKI.db.analytics.query().patch({
isEnabled: str.isEnabled,
config: reduce(str.config, (result, value, key) => {
set(result, `${value.key}`, get(JSON.parse(value.value), 'v', null))
return result
}, {})
}).where('key', str.key)
await WIKI.cache.del('analytics')
}
return {
responseResult: generateSuccess('Providers updated successfully')
}
} catch (err) {
return generateError(err)
}
}
}
}

@ -1,401 +0,0 @@
import _ from 'lodash-es'
import sanitize from 'sanitize-filename'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { decodeTreePath, generateHash } from '../../helpers/common.mjs'
import path from 'node:path'
import fs from 'fs-extra'
import { v4 as uuid } from 'uuid'
import { pipeline } from 'node:stream/promises'
export default {
Query: {
async assetById (obj, args, context) {
const asset = await WIKI.db.assets.query().findById(args.id).withGraphFetched('tree')
if (asset) {
const assetPath = asset.tree.folderPath ? `${decodeTreePath(asset.tree.folderPath)}/${asset.tree.fileName}` : asset.tree.fileName
if (!WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path: assetPath })) {
throw new Error('ERR_FORBIDDEN')
}
return asset
} else {
throw new Error('ERR_ASSET_NOT_FOUND')
}
}
},
Mutation: {
/**
* Rename an Asset
*/
async renameAsset (obj, args, context) {
try {
const filename = sanitize(args.fileName).toLowerCase()
const asset = await WIKI.db.assets.query().findById(args.id)
const treeItem = await WIKI.db.tree.query().findById(args.id)
if (asset && treeItem) {
// Check for extension mismatch
if (!_.endsWith(filename, asset.fileExt)) {
throw new Error('ERR_ASSET_EXT_MISMATCH')
}
// Check for non-dot files changing to dotfile
if (asset.fileExt.length > 0 && filename.length - asset.fileExt.length < 1) {
throw new Error('ERR_ASSET_INVALID_DOTFILE')
}
// Check for collision
const assetCollision = await WIKI.db.tree.query().where({
folderPath: treeItem.folderPath,
fileName: filename
}).first()
if (assetCollision) {
throw new Error('ERR_ASSET_ALREADY_EXISTS')
}
// Check source asset permissions
const assetSourcePath = (treeItem.folderPath) ? decodeTreePath(treeItem.folderPath) + `/${treeItem.fileName}` : treeItem.fileName
if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) {
throw new Error('ERR_FORBIDDEN')
}
// Check target asset permissions
const assetTargetPath = (treeItem.folderPath) ? decodeTreePath(treeItem.folderPath) + `/${filename}` : filename
if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) {
throw new Error('ERR_TARGET_FORBIDDEN')
}
// Update filename + hash
const itemHash = generateHash(assetTargetPath)
await WIKI.db.assets.query().patch({
fileName: filename
}).findById(asset.id)
await WIKI.db.tree.query().patch({
fileName: filename,
title: filename,
hash: itemHash
}).findById(treeItem.id)
// TODO: Delete old asset cache
WIKI.events.outbound.emit('purgeItemCache', itemHash)
// TODO: Rename in Storage
// await WIKI.db.storage.assetEvent({
// event: 'renamed',
// asset: {
// ...asset,
// path: assetSourcePath,
// destinationPath: assetTargetPath,
// moveAuthorId: context.req.user.id,
// moveAuthorName: context.req.user.name,
// moveAuthorEmail: context.req.user.email
// }
// })
return {
operation: generateSuccess('Asset has been renamed successfully.')
}
} else {
throw new Error('ERR_INVALID_ASSET')
}
} catch (err) {
return generateError(err)
}
},
/**
* Delete an Asset
*/
async deleteAsset (obj, args, context) {
try {
const treeItem = await WIKI.db.tree.query().findById(args.id)
if (treeItem) {
// Check permissions
const assetPath = (treeItem.folderPath) ? decodeTreePath(treeItem.folderPath) + `/${treeItem.fileName}` : treeItem.fileName
if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) {
throw new Error('ERR_FORBIDDEN')
}
// Delete from DB
await WIKI.db.assets.query().deleteById(treeItem.id)
await WIKI.db.tree.query().deleteById(treeItem.id)
// TODO: Delete asset cache
WIKI.events.outbound.emit('purgeItemCache', treeItem.hash)
// TODO: Delete from Storage
// await WIKI.db.storage.assetEvent({
// event: 'deleted',
// asset: {
// ...asset,
// path: assetPath,
// authorId: context.req.user.id,
// authorName: context.req.user.name,
// authorEmail: context.req.user.email
// }
// })
return {
operation: generateSuccess('Asset has been deleted successfully.')
}
} else {
throw new Error('ERR_INVALID_ASSET')
}
} catch (err) {
return generateError(err)
}
},
/**
* Upload Assets
*/
async uploadAssets (obj, args, context) {
try {
// FIXME: Perm
// -> Get Folder
let folder = {}
if (args.folderId || args.folderPath) {
// Get Folder by ID
folder = await WIKI.db.tree.getFolder({ id: args.folderId })
if (!folder) {
throw new Error('ERR_INVALID_FOLDER_ID')
}
} else if (args.folderPath) {
// Get Folder by Path
if (!args.locale) {
throw new Error('ERR_MISSING_LOCALE')
} else if (!args.siteId) {
throw new Error('ERR_MISSING_SITE_ID')
}
folder = await WIKI.db.tree.getFolder({
path: args.folderPath,
locale: args.locale,
siteId: args.siteId,
createIfMissing: true
})
if (!folder) {
throw new Error('ERR_INVALID_FOLDER_PATH')
}
} else {
// Use Root Folder
if (!args.locale) {
throw new Error('ERR_MISSING_LOCALE')
} else if (!args.siteId) {
throw new Error('ERR_MISSING_SITE_ID')
}
folder = {
folderPath: '',
fileName: '',
locale: args.locale,
siteId: args.siteId
}
}
// -> Get Site
const site = await WIKI.db.sites.query().findById(folder.siteId)
if (!site) {
throw new Error('ERR_INVALID_SITE_ID')
}
// -> Get Storage Targets
const storageTargets = await WIKI.db.storage.getTargets({ siteId: folder.siteId, enabledOnly: true })
// -> Process Assets
const results = await Promise.allSettled(args.files.map(async fl => {
const { filename, mimetype, createReadStream } = await fl
const sanitizedFilename = sanitize(filename).toLowerCase().trim()
WIKI.logger.debug(`Processing asset upload ${sanitizedFilename} of type ${mimetype}...`)
// Parse file extension
if (sanitizedFilename.indexOf('.') <= 0) {
throw new Error('ERR_ASSET_DOTFILE_NOTALLOWED')
}
const fileExt = _.last(sanitizedFilename.split('.')).toLowerCase()
// Determine asset kind
let fileKind = 'other'
switch (fileExt) {
case 'jpg':
case 'jpeg':
case 'png':
case 'webp':
case 'gif':
case 'tiff':
case 'svg':
fileKind = 'image'
break
case 'pdf':
case 'docx':
case 'xlsx':
case 'pptx':
case 'odt':
case 'epub':
case 'csv':
case 'md':
case 'txt':
case 'adoc':
case 'rtf':
case 'wdp':
case 'xps':
case 'ods':
fileKind = 'document'
break
}
// Save to temp disk
const tempFileId = uuid()
const tempFilePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads/${tempFileId}.dat`)
WIKI.logger.debug(`Writing asset upload ${sanitizedFilename} to temp disk...`)
await pipeline(
createReadStream(),
fs.createWriteStream(tempFilePath)
)
WIKI.logger.debug(`Querying asset ${sanitizedFilename} file size...`)
const tempFileStat = await fs.stat(tempFilePath)
// Format filename
const formattedFilename = site.config.uploads.normalizeFilename ? sanitizedFilename.replaceAll(' ', '-') : sanitizedFilename
// Save asset to DB
WIKI.logger.debug(`Saving asset ${sanitizedFilename} metadata to DB...`)
const assetRaw = await WIKI.db.knex('assets').insert({
fileName: formattedFilename,
fileExt,
kind: fileKind,
mimeType: mimetype,
fileSize: Math.round(tempFileStat.size),
meta: {},
previewState: fileKind === 'image' ? 'pending' : 'none',
authorId: context.req.user.id,
siteId: folder.siteId
}).returning('*')
const asset = assetRaw[0]
// Add to tree
await WIKI.db.tree.addAsset({
id: asset.id,
parentPath: folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName,
fileName: formattedFilename,
title: formattedFilename,
locale: folder.locale,
siteId: folder.siteId,
meta: {
authorId: asset.authorId,
creatorId: asset.creatorId,
fileSize: asset.fileSize,
fileExt,
mimeType: mimetype,
ownerId: asset.ownerId
}
})
// Save to storage targets
const storageInfo = {}
const failedStorage = []
await Promise.allSettled(storageTargets.map(async storageTarget => {
WIKI.logger.debug(`Saving asset ${sanitizedFilename} to storage target ${storageTarget.module} (${storageTarget.id})...`)
try {
const strInfo = await WIKI.storage.modules[storageTarget.module].assetUploaded({
asset,
createReadStream,
storageTarget,
tempFilePath
})
storageInfo[storageTarget.id] = strInfo ?? true
} catch (err) {
WIKI.logger.warn(`Failed to save asset ${sanitizedFilename} to storage target ${storageTarget.module} (${storageTarget.id}):`)
WIKI.logger.warn(err)
failedStorage.push({
storageId: storageTarget.id,
storageModule: storageTarget.module,
fileId: asset.id,
fileName: formattedFilename
})
}
}))
// Save Storage Info to DB
await WIKI.db.knex('assets').where({ id: asset.id }).update({ storageInfo })
// Create thumbnail
if (fileKind === 'image') {
if (!WIKI.extensions.ext.sharp.isInstalled) {
WIKI.logger.warn('Cannot generate asset thumbnail because the Sharp extension is not installed.')
} else {
WIKI.logger.debug(`Generating thumbnail of asset ${sanitizedFilename}...`)
const previewDestPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads/${tempFileId}-thumb.webp`)
// -> Resize
await WIKI.extensions.ext.sharp.resize({
format: 'webp',
inputStream: createReadStream(),
outputPath: previewDestPath,
width: 320,
height: 200,
fit: 'inside'
})
// -> Save to DB
await WIKI.db.knex('assets').where({
id: asset.id
}).update({
preview: await fs.readFile(previewDestPath),
previewState: 'ready'
})
// -> Delete
await fs.remove(previewDestPath)
}
}
WIKI.logger.debug(`Removing asset ${sanitizedFilename} temp file...`)
await fs.remove(tempFilePath)
WIKI.logger.debug(`Processed asset ${sanitizedFilename} successfully.`)
return failedStorage
}))
// Return results
const failedResults = results.filter(r => r.status === 'rejected')
if (failedResults.length > 0) {
// -> One or more thrown errors
WIKI.logger.warn('Failed to upload one or more assets:')
for (const failedResult of failedResults) {
WIKI.logger.warn(failedResult.reason)
}
throw new Error('ERR_UPLOAD_FAILED')
} else {
const failedSaveTargets = results.map(r => r.value).filter(r => r.length > 0)
if (failedSaveTargets.length > 0) {
// -> One or more storage target save errors
WIKI.logger.warn('Failed to save one or more assets to storage targets.')
throw new Error('ERR_UPLOAD_TARGET_FAILED')
} else {
WIKI.logger.debug('Asset(s) uploaded successfully.')
return {
operation: generateSuccess('Asset(s) uploaded successfully')
}
}
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* Flush Temporary Uploads
*/
async flushTempUploads (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.assets.flushTempUploads()
return {
operation: generateSuccess('Temporary Uploads have been flushed successfully.')
}
} catch (err) {
return generateError(err)
}
}
}
}

@ -1,741 +0,0 @@
import _ from 'lodash-es'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import jwt from 'jsonwebtoken'
import ms from 'ms'
import { DateTime } from 'luxon'
import { base64 } from '@hexagon/base64'
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server'
import { isoBase64URL } from '@simplewebauthn/server/helpers'
export default {
Query: {
/**
* List of API Keys
*/
async apiKeys (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:api', 'manage:api'])) {
throw new Error('ERR_FORBIDDEN')
}
const keys = await WIKI.db.apiKeys.query().orderBy(['isRevoked', 'name'])
return keys.map(k => ({
id: k.id,
name: k.name,
keyShort: '...' + k.key.substring(k.key.length - 20),
isRevoked: k.isRevoked,
expiration: k.expiration,
createdAt: k.createdAt,
updatedAt: k.updatedAt
}))
},
/**
* Current API State
*/
apiState (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:api', 'manage:api', 'read:dashboard'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.config.api.isEnabled
},
/**
* Fetch authentication strategies
*/
async authStrategies (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.data.authentication.map(stg => ({
...stg,
isAvailable: stg.isAvailable === true
}))
},
/**
* Fetch active authentication strategies
*/
async authActiveStrategies (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
const strategies = await WIKI.db.authentication.getStrategies({ enabledOnly: args.enabledOnly })
return strategies.map(a => {
const str = _.find(WIKI.data.authentication, ['key', a.module]) || {}
return {
...a,
config: _.transform(str.props, (r, v, k) => {
r[k] = v.sensitive ? '********' : a.config[k]
}, {})
}
})
},
/**
* Fetch site authentication strategies
*/
async authSiteStrategies (obj, args, context, info) {
const site = await WIKI.db.sites.query().findById(args.siteId)
const activeStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
const siteStrategies = _.sortBy(activeStrategies.map(str => {
const siteAuth = _.find(site.config.authStrategies, ['id', str.id]) || {}
return {
id: str.id,
activeStrategy: str,
order: siteAuth.order ?? 0,
isVisible: siteAuth.isVisible ?? false
}
}), ['order'])
return args.visibleOnly ? siteStrategies.filter(s => s.isVisible) : siteStrategies
}
},
Mutation: {
/**
* Create New API Key
*/
async createApiKey (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) {
throw new Error('ERR_FORBIDDEN')
}
const key = await WIKI.db.apiKeys.createNewKey(args)
await WIKI.auth.reloadApiKeys()
WIKI.events.outbound.emit('reloadApiKeys')
return {
key,
operation: generateSuccess('API Key created successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* Perform Login
*/
async login (obj, args, context) {
try {
const authResult = await WIKI.db.users.login(args, context)
return {
...authResult,
operation: generateSuccess('Login success')
}
} catch (err) {
// LDAP Debug Flag
if (args.strategy === 'ldap' && WIKI.config.flags.ldapdebug) {
WIKI.logger.warn('LDAP LOGIN ERROR (c1): ', err)
}
WIKI.logger.debug(err)
return generateError(err)
}
},
/**
* Perform 2FA Login
*/
async loginTFA (obj, args, context) {
try {
const authResult = await WIKI.db.users.loginTFA(args, context)
return {
...authResult,
operation: generateSuccess('TFA success')
}
} catch (err) {
WIKI.logger.debug(err)
return generateError(err)
}
},
/**
* Setup TFA
*/
async setupTFA (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
const str = WIKI.auth.strategies[args.strategyId]
if (!str) {
throw new Error('ERR_INVALID_STRATEGY')
}
if (!usr.auth[args.strategyId]) {
throw new Error('ERR_INVALID_STRATEGY')
}
if (usr.auth[args.strategyId].tfaIsActive) {
throw new Error('ERR_TFA_ALREADY_ACTIVE')
}
const tfaQRImage = await usr.generateTFA(args.strategyId, args.siteId)
const tfaToken = await WIKI.db.userKeys.generateToken({
kind: 'tfaSetup',
userId: usr.id,
meta: {
strategyId: args.strategyId
}
})
return {
operation: generateSuccess('TFA setup started'),
continuationToken: tfaToken,
tfaQRImage
}
} catch (err) {
return generateError(err)
}
},
/**
* Deactivate 2FA
*/
async deactivateTFA (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
const str = WIKI.auth.strategies[args.strategyId]
if (!str) {
throw new Error('ERR_INVALID_STRATEGY')
}
if (!usr.auth[args.strategyId]) {
throw new Error('ERR_INVALID_STRATEGY')
}
if (!usr.auth[args.strategyId].tfaIsActive) {
throw new Error('ERR_TFA_NOT_ACTIVE')
}
usr.auth[args.strategyId].tfaIsActive = false
usr.auth[args.strategyId].tfaSecret = null
await usr.$query().patch({
auth: usr.auth
})
return {
operation: generateSuccess('TFA deactivated successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* Setup Passkey
*/
async setupPasskey (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
const site = WIKI.sites[args.siteId]
if (!site) {
throw new Error('ERR_INVALID_SITE')
} else if (site.hostname === '*') {
WIKI.logger.warn('Cannot use passkeys with a wildcard site hostname. Enter a valid hostname under the Administration Area > General.')
throw new Error('ERR_PK_HOSTNAME_MISSING')
}
const options = await generateRegistrationOptions({
rpName: site.config.title,
rpId: site.hostname,
userID: usr.id,
userName: usr.email,
userDisplayName: usr.name,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'required',
userVerification: 'preferred'
},
excludeCredentials: usr.passkeys.authenticators?.map(authenticator => ({
id: isoBase64URL.fromBuffer(new Uint8Array(authenticator.credentialID)),
type: 'public-key',
transports: authenticator.transports
})) ?? []
})
usr.passkeys.reg = {
challenge: options.challenge,
rpId: site.hostname,
siteId: site.id
}
await usr.$query().patch({
passkeys: usr.passkeys
})
return {
operation: generateSuccess('Passkey registration options generated successfully.'),
registrationOptions: options
}
} catch (err) {
return generateError(err)
}
},
/**
* Finalize Passkey Registration
*/
async finalizePasskey (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
} else if (!usr.passkeys?.reg) {
throw new Error('ERR_PASSKEY_NOT_SETUP')
}
if (!args.name || args.name.trim().length < 1 || args.name.length > 255) {
throw new Error('ERR_PK_NAME_MISSING_OR_INVALID')
}
const verification = await verifyRegistrationResponse({
response: args.registrationResponse,
expectedChallenge: usr.passkeys.reg.challenge,
expectedOrigin: `https://${usr.passkeys.reg.rpId}`,
expectedRPID: usr.passkeys.reg.rpId,
requireUserVerification: true
})
if (!verification.verified) {
throw new Error('ERR_PK_VERIFICATION_FAILED')
}
if (!usr.passkeys.authenticators) {
usr.passkeys.authenticators = []
}
usr.passkeys.authenticators.push({
...verification.registrationInfo,
id: base64.fromArrayBuffer(verification.registrationInfo.credentialID, true),
createdAt: new Date(),
name: args.name,
siteId: usr.passkeys.reg.siteId,
transports: args.registrationResponse.response.transports
})
delete usr.passkeys.reg
await usr.$query().patch({
passkeys: JSON.stringify(usr.passkeys, (k, v) => {
if (v instanceof Uint8Array) {
return Array.apply([], v)
}
return v
})
})
return {
operation: generateSuccess('Passkey registered successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* Deactivate a passkey
*/
async deactivatePasskey (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
} else if (!usr.passkeys?.authenticators) {
throw new Error('ERR_PASSKEY_NOT_SETUP')
}
usr.passkeys.authenticators = usr.passkeys.authenticators.filter(a => a.id !== args.id)
await usr.$query().patch({
passkeys: usr.passkeys
})
return {
operation: generateSuccess('Passkey deactivated successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* Login via passkey - Generate challenge
*/
async authenticatePasskeyGenerate (obj, args, context) {
try {
const site = WIKI.sites[args.siteId]
if (!site) {
throw new Error('ERR_INVALID_SITE')
} else if (site.hostname === '*') {
WIKI.logger.warn('Cannot use passkeys with a wildcard site hostname. Enter a valid hostname under the Administration Area > General.')
throw new Error('ERR_PK_HOSTNAME_MISSING')
}
const usr = await WIKI.db.users.query().findOne({ email: args.email })
if (!usr || !usr.passkeys?.authenticators) {
// Fake success response to prevent email leaking
WIKI.logger.debug(`Cannot generate passkey challenge for ${args.email}... (non-existing or missing passkeys setup)`)
return {
operation: generateSuccess('Passkey challenge generated.'),
authOptions: await generateAuthenticationOptions({
allowCredentials: [{
id: new Uint8Array(Array(30).map(v => _.random(0, 254))),
type: 'public-key',
transports: ['internal']
}],
userVerification: 'preferred',
rpId: site.hostname
})
}
}
const options = await generateAuthenticationOptions({
allowCredentials: usr.passkeys.authenticators.map(authenticator => ({
id: new Uint8Array(authenticator.credentialID),
type: 'public-key',
transports: authenticator.transports
})),
userVerification: 'preferred',
rpId: site.hostname
})
usr.passkeys.login = {
challenge: options.challenge,
rpId: site.hostname,
siteId: site.id
}
await usr.$query().patch({
passkeys: usr.passkeys
})
return {
operation: generateSuccess('Passkey challenge generated.'),
authOptions: options
}
} catch (err) {
return generateError(err)
}
},
/**
* Login via passkey - Verify challenge
*/
async authenticatePasskeyVerify (obj, args, context) {
try {
if (!args.authResponse?.response?.userHandle) {
throw new Error('ERR_INVALID_PASSKEY_RESPONSE')
}
const usr = await WIKI.db.users.query().findById(args.authResponse.response.userHandle)
if (!usr) {
WIKI.logger.debug(`Passkey Login Failure: Cannot find user ${args.authResponse.response.userHandle}`)
throw new Error('ERR_LOGIN_FAILED')
} else if (!usr.passkeys?.login) {
WIKI.logger.debug(`Passkey Login Failure: Missing login auth generation step for user ${args.authResponse.response.userHandle}`)
throw new Error('ERR_LOGIN_FAILED')
} else if (!usr.passkeys.authenticators?.some(a => a.id === args.authResponse.id)) {
WIKI.logger.debug(`Passkey Login Failure: Authenticator provided is not registered for user ${args.authResponse.response.userHandle}`)
throw new Error('ERR_LOGIN_FAILED')
}
const verification = await verifyAuthenticationResponse({
response: args.authResponse,
expectedChallenge: usr.passkeys.login.challenge,
expectedOrigin: `https://${usr.passkeys.login.rpId}`,
expectedRPID: usr.passkeys.login.rpId,
requireUserVerification: true,
authenticator: _.find(usr.passkeys.authenticators, ['id', args.authResponse.id])
})
if (!verification.verified) {
WIKI.logger.debug(`Passkey Login Failure: Challenge verification failed for user ${args.authResponse.response.userHandle}`)
throw new Error('ERR_LOGIN_FAILED')
}
delete usr.passkeys.login
await usr.$query().patch({
passkeys: usr.passkeys
})
const jwtToken = await WIKI.db.users.refreshToken(usr)
return {
operation: generateSuccess('Passkey challenge accepted.'),
nextAction: 'redirect',
jwt: jwtToken.token,
redirect: '/'
}
} catch (err) {
return generateError(err)
}
},
/**
* Perform Password Change
*/
async changePassword (obj, args, context) {
try {
if (args.continuationToken) {
const authResult = await WIKI.db.users.loginChangePassword(args, context)
return {
...authResult,
operation: generateSuccess('Password set successfully')
}
} else {
await WIKI.db.users.changePassword(args, context)
return {
operation: generateSuccess('Password changed successfully')
}
}
} catch (err) {
WIKI.logger.debug(err)
return generateError(err)
}
},
/**
* Perform Forget Password
*/
async forgotPassword (obj, args, context) {
try {
await WIKI.db.users.loginForgotPassword(args, context)
return {
operation: generateSuccess('Password reset request processed.')
}
} catch (err) {
return generateError(err)
}
},
/**
* Register a new account
*/
async register (obj, args, context) {
try {
const usr = await WIKI.db.users.createNewUser({ ...args, userInitiated: true })
const authResult = await WIKI.db.users.afterLoginChecks(usr, WIKI.data.systemIds.localAuthId, context)
return {
...authResult,
operation: generateSuccess('Registration success')
}
} catch (err) {
return generateError(err)
}
},
/**
* Refresh Token
*/
async refreshToken (obj, args, context) {
try {
let decoded = {}
if (!args.token) {
throw new Error('ERR_MISSING_TOKEN')
}
try {
decoded = jwt.verify(args.token, WIKI.config.auth.certs.public, {
audience: WIKI.config.auth.audience,
issuer: 'urn:wiki.js',
algorithms: ['RS256'],
ignoreExpiration: true
})
} catch (err) {
throw new Error('ERR_INVALID_TOKEN')
}
if (DateTime.utc().minus(ms(WIKI.config.auth.tokenRenewal)) > DateTime.fromSeconds(decoded.exp)) {
throw new Error('ERR_EXPIRED_TOKEN')
}
const newToken = await WIKI.db.users.refreshToken(decoded.id)
return {
jwt: newToken.token,
operation: generateSuccess('Token refreshed successfully')
}
} catch (err) {
return generateError(err)
}
},
/**
* Set API state
*/
async setApiState (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.api.isEnabled = args.enabled
await WIKI.configSvc.saveToDb(['api'])
return {
operation: generateSuccess('API State changed successfully')
}
} catch (err) {
return generateError(err)
}
},
/**
* Revoke an API key
*/
async revokeApiKey (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.apiKeys.query().findById(args.id).patch({
isRevoked: true
})
await WIKI.auth.reloadApiKeys()
WIKI.events.outbound.emit('reloadApiKeys')
return {
operation: generateSuccess('API Key revoked successfully')
}
} catch (err) {
return generateError(err)
}
},
/**
* Update Authentication Strategies
*/
async updateAuthStrategies (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
const previousStrategies = await WIKI.db.authentication.getStrategies()
for (const str of args.strategies) {
const newStr = {
displayName: str.displayName,
isEnabled: str.isEnabled,
config: _.reduce(str.config, (result, value, key) => {
_.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
return result
}, {}),
selfRegistration: str.selfRegistration,
domainWhitelist: { v: str.domainWhitelist },
autoEnrollGroups: { v: str.autoEnrollGroups }
}
if (_.some(previousStrategies, ['key', str.key])) {
await WIKI.db.authentication.query().patch({
key: str.key,
strategyKey: str.strategyKey,
...newStr
}).where('key', str.key)
} else {
await WIKI.db.authentication.query().insert({
key: str.key,
strategyKey: str.strategyKey,
...newStr
})
}
}
for (const str of _.differenceBy(previousStrategies, args.strategies, 'key')) {
const hasUsers = await WIKI.db.users.query().count('* as total').where({ providerKey: str.key }).first()
if (_.toSafeInteger(hasUsers.total) > 0) {
throw new Error(`Cannot delete ${str.displayName} as 1 or more users are still using it.`)
} else {
await WIKI.db.authentication.query().delete().where('key', str.key)
}
}
await WIKI.auth.activateStrategies()
WIKI.events.outbound.emit('reloadAuthStrategies')
return {
responseResult: generateSuccess('Strategies updated successfully')
}
} catch (err) {
return generateError(err)
}
},
/**
* Generate New Authentication Public / Private Key Certificates
*/
async regenerateCertificates (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.auth.regenerateCertificates()
return {
responseResult: generateSuccess('Certificates have been regenerated successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* Reset Guest User
*/
async resetGuestUser (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.auth.resetGuestUser()
return {
responseResult: generateSuccess('Guest user has been reset successfully.')
}
} catch (err) {
return generateError(err)
}
}
},
// ------------------------------------------------------------------
// TYPE: AuthenticationActiveStrategy
// ------------------------------------------------------------------
AuthenticationActiveStrategy: {
config (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return obj.config ?? {}
},
allowedEmailRegex (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return obj.allowedEmailRegex ?? ''
},
autoEnrollGroups (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return obj.autoEnrollGroups ?? []
},
strategy (obj, args, context) {
return _.find(WIKI.data.authentication, ['key', obj.module])
}
}
}

@ -1,26 +0,0 @@
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
export default {
Query: {
async blocks (obj, args, context) {
return WIKI.db.blocks.query().where({
siteId: args.siteId
})
}
},
Mutation: {
async setBlocksState(obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:blocks'])) {
throw new Error('ERR_FORBIDDEN')
}
// TODO: update blocks state
return {
operation: generateSuccess('Blocks state updated successfully')
}
} catch (err) {
return generateError(err)
}
}
}
}

@ -1,166 +0,0 @@
import _ from 'lodash-es'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
export default {
Query: {
/**
* Fetch list of Comments Providers
*/
async commentsProviders(obj, args, context, info) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
const providers = await WIKI.db.commentProviders.getProviders()
return providers.map(provider => {
const providerInfo = _.find(WIKI.data.commentProviders, ['key', provider.key]) || {}
return {
...providerInfo,
...provider,
config: _.sortBy(_.transform(provider.config, (res, value, key) => {
const configData = _.get(providerInfo.props, key, false)
if (configData) {
res.push({
key,
value: JSON.stringify({
...configData,
value
})
})
}
}, []), 'key')
}
})
},
/**
* Fetch list of comments for a page
*/
async comments (obj, args, context) {
const page = await WIKI.db.pages.query().select('id').findOne({ locale: args.locale, path: args.path })
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], args)) {
const comments = await WIKI.db.comments.query().where('pageId', page.id).orderBy('createdAt')
return comments.map(c => ({
...c,
authorName: c.name,
authorEmail: c.email,
authorIP: c.ip
}))
} else {
throw new WIKI.Error.CommentViewForbidden()
}
} else {
return []
}
},
/**
* Fetch a single comment
*/
async commentById (obj, args, context) {
const cm = await WIKI.data.commentProvider.getCommentById(args.id)
if (!cm || !cm.pageId) {
throw new WIKI.Error.CommentNotFound()
}
const page = await WIKI.db.pages.query().select('locale', 'path').findById(cm.pageId)
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], {
path: page.path,
locale: page.locale
})) {
return {
...cm,
authorName: cm.name,
authorEmail: cm.email,
authorIP: cm.ip
}
} else {
throw new WIKI.Error.CommentViewForbidden()
}
} else {
WIKI.logger.warn(`Comment #${cm.id} is linked to a page #${cm.pageId} that doesn't exist! [ERROR]`)
throw new WIKI.Error.CommentGenericError()
}
}
},
Mutation: {
/**
* Create New Comment
*/
async createComment (obj, args, context) {
try {
const cmId = await WIKI.db.comments.postNewComment({
...args,
user: context.req.user,
ip: context.req.ip
})
return {
responseResult: generateSuccess('New comment posted successfully'),
id: cmId
}
} catch (err) {
return generateError(err)
}
},
/**
* Update an Existing Comment
*/
async updateComment (obj, args, context) {
try {
const cmRender = await WIKI.db.comments.updateComment({
...args,
user: context.req.user,
ip: context.req.ip
})
return {
responseResult: generateSuccess('Comment updated successfully'),
render: cmRender
}
} catch (err) {
return generateError(err)
}
},
/**
* Delete an Existing Comment
*/
async deleteComment (obj, args, context) {
try {
await WIKI.db.comments.deleteComment({
id: args.id,
user: context.req.user,
ip: context.req.ip
})
return {
responseResult: generateSuccess('Comment deleted successfully')
}
} catch (err) {
return generateError(err)
}
},
/**
* Update Comments Providers
*/
async updateCommentsProviders(obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
for (let provider of args.providers) {
await WIKI.db.commentProviders.query().patch({
isEnabled: provider.isEnabled,
config: _.reduce(provider.config, (result, value, key) => {
_.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
return result
}, {})
}).where('key', provider.key)
}
await WIKI.db.commentProviders.initProvider()
return {
responseResult: generateSuccess('Comment Providers updated successfully')
}
} catch (err) {
return generateError(err)
}
}
}
}

@ -1,231 +0,0 @@
import { generateSuccess } from '../../helpers/graph.mjs'
import safeRegex from 'safe-regex'
import _ from 'lodash-es'
import { v4 as uuid } from 'uuid'
export default {
Query: {
/**
* FETCH ALL GROUPS
*/
async groups (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:groups', 'manage:users', 'manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.db.groups.query().select(
'groups.*',
WIKI.db.groups.relatedQuery('users').count().as('userCount')
)
},
/**
* FETCH A SINGLE GROUP
*/
async groupById(obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:groups', 'manage:users', 'manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.db.groups.query().findById(args.id)
}
},
Mutation: {
/**
* ASSIGN USER TO GROUP
*/
async assignUserToGroup (obj, args, { req }) {
if (!WIKI.auth.checkAccess(req.user, ['manage:groups', 'manage:users', 'manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
// Check for guest user
if (args.userId === WIKI.config.auth.guestUserId) {
throw new Error('Cannot assign the Guest user to a group.')
}
// Check for valid group
const grp = await WIKI.db.groups.query().findById(args.groupId)
if (!grp) {
throw new Error('Invalid Group ID')
}
// Check assigned permissions for write:groups
if (
WIKI.auth.checkExclusiveAccess(req.user, ['write:groups'], ['manage:groups', 'manage:system']) &&
grp.permissions.some(p => {
const resType = _.last(p.split(':'))
return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType)
})
) {
throw new Error('You are not authorized to assign a user to this elevated group.')
}
// Check for valid user
const usr = await WIKI.db.users.query().findById(args.userId)
if (!usr) {
throw new Error('Invalid User ID')
}
// Check for existing relation
const relExist = await WIKI.db.knex('userGroups').where({
userId: args.userId,
groupId: args.groupId
}).first()
if (relExist) {
throw new Error('User is already assigned to group.')
}
// Assign user to group
await grp.$relatedQuery('users').relate(usr.id)
// Revoke tokens for this user
WIKI.auth.revokeUserTokens({ id: usr.id, kind: 'u' })
WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })
return {
operation: generateSuccess('User has been assigned to group.')
}
},
/**
* CREATE NEW GROUP
*/
async createGroup (obj, args, { req }) {
if (!WIKI.auth.checkAccess(req.user, ['manage:groups', 'manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
const group = await WIKI.db.groups.query().insertAndFetch({
name: args.name,
permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),
rules: JSON.stringify(WIKI.data.groups.defaultRules.map(r => ({
id: uuid(),
...r
}))),
isSystem: false
})
await WIKI.auth.reloadGroups()
WIKI.events.outbound.emit('reloadGroups')
return {
operation: generateSuccess('Group created successfully.'),
group
}
},
/**
* DELETE GROUP
*/
async deleteGroup (obj, args, { req }) {
if (!WIKI.auth.checkAccess(req.user, ['manage:groups', 'manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
if (args.id === WIKI.data.systemIds.guestsGroupId || args.id === WIKI.data.systemIds.usersGroupId || args.id === WIKI.config.auth.rootAdminGroupId) {
throw new Error('Cannot delete this group.')
}
await WIKI.db.groups.query().deleteById(args.id)
WIKI.auth.revokeUserTokens({ id: args.id, kind: 'g' })
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'g' })
await WIKI.auth.reloadGroups()
WIKI.events.outbound.emit('reloadGroups')
return {
operation: generateSuccess('Group has been deleted.')
}
},
/**
* UNASSIGN USER FROM GROUP
*/
async unassignUserFromGroup (obj, args, { req }) {
if (!WIKI.auth.checkAccess(req.user, ['manage:groups', 'manage:users', 'manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
if (args.userId === 2) {
throw new Error('Cannot unassign Guest user')
}
if (args.userId === WIKI.config.auth.guestUserId && args.groupId === WIKI.data.systemIds.guestsGroupId) {
throw new Error('Cannot unassign Administrator user from Administrators group.')
}
const grp = await WIKI.db.groups.query().findById(args.groupId)
if (!grp) {
throw new Error('Invalid Group ID')
}
const usr = await WIKI.db.users.query().findById(args.userId)
if (!usr) {
throw new Error('Invalid User ID')
}
await grp.$relatedQuery('users').unrelate().where('userId', usr.id)
WIKI.auth.revokeUserTokens({ id: usr.id, kind: 'u' })
WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })
return {
operation: generateSuccess('User has been unassigned from group.')
}
},
/**
* UPDATE GROUP
*/
async updateGroup (obj, args, { req }) {
if (!WIKI.auth.checkAccess(req.user, ['manage:groups', 'manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
// Check for unsafe regex page rules
if (_.some(args.pageRules, pr => {
return pr.match === 'REGEX' && !safeRegex(pr.path)
})) {
throw new Error('Some Page Rules contains unsafe or exponential time regex.')
}
// Set default redirect on login value
if (_.isEmpty(args.redirectOnLogin)) {
args.redirectOnLogin = '/'
}
// Check assigned permissions for write:groups
if (
WIKI.auth.checkExclusiveAccess(req.user, ['write:groups'], ['manage:groups', 'manage:system']) &&
args.permissions.some(p => {
const resType = _.last(p.split(':'))
return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType)
})
) {
throw new Error('You are not authorized to manage this group or assign these permissions.')
}
// Check assigned permissions for manage:groups
if (
WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage:system']) &&
args.permissions.some(p => _.last(p.split(':')) === 'system')
) {
throw new Error('You are not authorized to manage this group or assign the manage:system permissions.')
}
// Update group
await WIKI.db.groups.query().patch({
name: args.name,
redirectOnLogin: args.redirectOnLogin,
permissions: JSON.stringify(args.permissions),
pageRules: JSON.stringify(args.pageRules)
}).where('id', args.id)
// Revoke tokens for this group
WIKI.auth.revokeUserTokens({ id: args.id, kind: 'g' })
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'g' })
// Reload group permissions
await WIKI.auth.reloadGroups()
WIKI.events.outbound.emit('reloadGroups')
return {
operation: generateSuccess('Group has been updated.')
}
}
},
Group: {
users (grp) {
return grp.$relatedQuery('users')
}
}
}

@ -1,107 +0,0 @@
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import _ from 'lodash-es'
export default {
Query: {
async hooks (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:webhooks', 'write:webhooks', 'manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.db.hooks.query().orderBy('name')
},
async hookById (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:webhooks', 'write:webhooks', 'manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.db.hooks.query().findById(args.id)
}
},
Mutation: {
/**
* CREATE HOOK
*/
async createHook (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['write:webhooks', 'manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Validate inputs
if (!args.name || args.name.length < 1) {
throw new WIKI.Error.Custom('HookCreateInvalidName', 'Invalid Hook Name')
}
if (!args.events || args.events.length < 1) {
throw new WIKI.Error.Custom('HookCreateInvalidEvents', 'Invalid Hook Events')
}
if (!args.url || args.url.length < 8 || !args.url.startsWith('http')) {
throw new WIKI.Error.Custom('HookCreateInvalidURL', 'Invalid Hook URL')
}
// -> Create hook
const newHook = await WIKI.db.hooks.createHook(args)
WIKI.logger.debug(`New Hook ${newHook.id} created successfully.`)
return {
operation: generateSuccess('Hook created successfully'),
hook: newHook
}
} catch (err) {
return generateError(err)
}
},
/**
* UPDATE HOOK
*/
async updateHook (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Load hook
const hook = await WIKI.db.hooks.query().findById(args.id)
if (!hook) {
throw new WIKI.Error.Custom('HookInvalidId', 'Invalid Hook ID')
}
// -> Check for bad input
if (_.has(args.patch, 'name') && args.patch.name.length < 1) {
throw new WIKI.Error.Custom('HookCreateInvalidName', 'Invalid Hook Name')
}
if (_.has(args.patch, 'events') && args.patch.events.length < 1) {
throw new WIKI.Error.Custom('HookCreateInvalidEvents', 'Invalid Hook Events')
}
if (_.has(args.patch, 'url') && (_.trim(args.patch.url).length < 8 || !args.patch.url.startsWith('http'))) {
throw new WIKI.Error.Custom('HookInvalidURL', 'URL is invalid.')
}
// -> Update hook
await WIKI.db.hooks.query().findById(args.id).patch(args.patch)
WIKI.logger.debug(`Hook ${args.id} updated successfully.`)
return {
operation: generateSuccess('Hook updated successfully')
}
} catch (err) {
return generateError(err)
}
},
/**
* DELETE HOOK
*/
async deleteHook (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:webhooks'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.hooks.deleteHook(args.id)
WIKI.logger.debug(`Hook ${args.id} deleted successfully.`)
return {
operation: generateSuccess('Hook deleted successfully')
}
} catch (err) {
return generateError(err)
}
}
}
}

@ -1,10 +0,0 @@
export default {
Query: {
async locales(obj, args, context, info) {
return WIKI.db.locales.getLocales({ cache: false })
},
localeStrings (obj, args, context, info) {
return WIKI.db.locales.getStrings(args.locale)
}
}
}

@ -1,78 +0,0 @@
import _ from 'lodash-es'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { withoutTrailingSlash } from 'ufo'
export default {
Query: {
async mailConfig(obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return {
...WIKI.config.mail,
pass: WIKI.config.mail.pass.length > 0 ? '********' : ''
}
}
},
Mutation: {
async sendMailTest(obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
if (_.isEmpty(args.recipientEmail) || args.recipientEmail.length < 6) {
throw new Error('ERR_MAIL_INVALID_RECIPIENT')
}
await WIKI.mail.send({
template: 'Test',
to: args.recipientEmail,
subject: 'A test email from your wiki',
text: 'This is a test email sent from your wiki.',
data: {}
})
return {
operation: generateSuccess('Test email sent successfully.')
}
} catch (err) {
return generateError(err)
}
},
async updateMailConfig(obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.mail = {
senderName: args.senderName,
senderEmail: args.senderEmail,
defaultBaseURL: withoutTrailingSlash(args.defaultBaseURL),
host: args.host,
port: args.port,
name: args.name,
secure: args.secure,
verifySSL: args.verifySSL,
user: args.user,
pass: (args.pass === '********') ? WIKI.config.mail.pass : args.pass,
useDKIM: args.useDKIM,
dkimDomainName: args.dkimDomainName,
dkimKeySelector: args.dkimKeySelector,
dkimPrivateKey: args.dkimPrivateKey
}
await WIKI.configSvc.saveToDb(['mail'])
WIKI.mail.init()
return {
operation: generateSuccess('Mail configuration updated successfully.')
}
} catch (err) {
return generateError(err)
}
}
}
}

@ -1,131 +0,0 @@
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { isNil } from 'lodash-es'
export default {
Query: {
async navigationById (obj, args, context, info) {
return WIKI.db.navigation.getNav({ id: args.id, cache: true, userGroups: context.req.user?.groups })
}
},
Mutation: {
async updateNavigation (obj, args, context) {
try {
let updateInherited = false
let updateInheritedNavId = null
let updateNavId = null
let ancestorNavId = null
const treeEntry = await WIKI.db.knex('tree').where('id', args.pageId).first()
if (!treeEntry) {
throw new Error('Invalid ID')
}
const currentNavId = treeEntry.folderPath === '' && treeEntry.fileName === 'home' ? treeEntry.siteId : treeEntry.id
const treeEntryPath = treeEntry.folderPath ? `${treeEntry.folderPath}.${treeEntry.fileName}` : treeEntry.fileName
// -> Create / Update Nav Menu Items
if (!isNil(args.items)) {
await WIKI.db.knex('navigation').insert({
id: currentNavId,
items: JSON.stringify(args.items),
siteId: treeEntry.siteId
}).onConflict('id').merge({
items: JSON.stringify(args.items)
})
}
// -> Find ancestor nav ID
const ancNavResult = await WIKI.db.knex.raw(`
SELECT "navigationId", "navigationMode", nlevel("folderPath" || "fileName") AS levels
FROM tree
WHERE ("folderPath" || "fileName") @> :currentPath
AND "navigationMode" IN ('override', 'hide')
ORDER BY levels DESC
LIMIT 1
`, {
currentPath: treeEntry.folderPath
})
if (ancNavResult.rowCount > 0) {
ancestorNavId = ancNavResult.rows[0]?.navigationId
} else {
ancestorNavId = treeEntry.siteId
}
// -> Update mode
switch (args.mode) {
case 'inherit': {
updateNavId = ancestorNavId
if (['override', 'hide'].includes(treeEntry.navigationMode)) {
updateInherited = true
updateInheritedNavId = ancestorNavId
}
break
}
case 'override': {
updateNavId = treeEntry.id
updateInherited = true
updateInheritedNavId = treeEntry.id
break
}
case 'overrideExact': {
updateNavId = treeEntry.id
if (['override', 'hide'].includes(treeEntry.navigationMode)) {
updateInherited = true
updateInheritedNavId = ancestorNavId
}
break
}
case 'hide': {
updateInherited = true
updateNavId = null
break
}
case 'hideExact': {
updateNavId = null
if (['override', 'hide'].includes(treeEntry.navigationMode)) {
updateInherited = true
updateInheritedNavId = ancestorNavId
}
break
}
}
// -> Set for current path
await WIKI.db.knex('tree').where('id', treeEntry.id).update({ navigationMode: args.mode, navigationId: updateNavId })
// -> Update nodes that inherit from current
if (updateInherited) {
await WIKI.db.knex.raw(`
UPDATE tree tt
SET "navigationId" = :navId
WHERE type IN ('page', 'folder')
AND "folderPath" <@ :overridePath
AND "navigationMode" = 'inherit'
AND NOT EXISTS (
SELECT 1
FROM tree tc
WHERE type IN ('page', 'folder')
AND tc."folderPath" <@ :overridePath
AND tc."folderPath" @> tt."folderPath"
AND tc."navigationMode" IN ('override', 'hide')
)
`, {
navId: updateInheritedNavId,
overridePath: treeEntryPath
})
}
// for (const tree of args.tree) {
// await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300)
// }
return {
operation: generateSuccess('Navigation updated successfully'),
navigationMode: args.mode,
navigationId: updateNavId
}
} catch (err) {
return generateError(err)
}
}
}
}

@ -1,709 +0,0 @@
import _ from 'lodash-es'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { parsePath } from '../../helpers/page.mjs'
import tsquery from 'pg-tsquery'
const tsq = tsquery()
const tagsInQueryRgx = /#[a-z0-9-\u3400-\u4DBF\u4E00-\u9FFF]+(?=(?:[^"]*(?:")[^"]*(?:"))*[^"]*$)/g
export default {
Query: {
/**
* PAGE HISTORY
*/
async pageHistoryById (obj, args, context, info) {
const page = await WIKI.db.pages.query().select('path', 'locale').findById(args.id)
if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {
path: page.path,
locale: page.locale
})) {
return WIKI.db.pageHistory.getHistory({
pageId: args.id,
offsetPage: args.offsetPage || 0,
offsetSize: args.offsetSize || 100
})
} else {
throw new WIKI.Error.PageHistoryForbidden()
}
},
/**
* PAGE VERSION
*/
async pageVersionById (obj, args, context, info) {
const page = await WIKI.db.pages.query().select('path', 'locale').findById(args.pageId)
if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {
path: page.path,
locale: page.locale
})) {
return WIKI.db.pageHistory.getVersion({
pageId: args.pageId,
versionId: args.versionId
})
} else {
throw new WIKI.Error.PageHistoryForbidden()
}
},
/**
* SEARCH PAGES
*/
async searchPages (obj, args, context) {
const q = args.query.trim()
const hasQuery = q.length > 0
// -> Validate parameters
if (!args.siteId) {
throw new Error('Missing Site ID')
}
if (args.offset && args.offset < 0) {
throw new Error('Invalid offset value.')
}
if (args.limit && (args.limit < 1 || args.limit > 100)) {
throw new Error('Limit must be between 1 and 100.')
}
try {
const dictName = 'english' // TODO: Use provided locale or fallback on site locale
// -> Select Columns
const searchCols = [
'id',
'path',
'locale',
'title',
'description',
'icon',
'tags',
'updatedAt',
WIKI.db.knex.raw('count(*) OVER() AS total')
]
// -> Set relevancy
if (hasQuery) {
searchCols.push(WIKI.db.knex.raw('ts_rank_cd(ts, query) AS relevancy'))
} else {
args.orderBy = args.orderBy === 'relevancy' ? 'title' : args.orderBy
}
// -> Add Highlighting if enabled
if (WIKI.config.search.termHighlighting && hasQuery) {
searchCols.push(WIKI.db.knex.raw('ts_headline(?, "searchContent", query, \'MaxWords=5, MinWords=3, MaxFragments=5\') AS highlight', [dictName]))
}
const results = await WIKI.db.knex
.select(searchCols)
.fromRaw(hasQuery ? 'pages, to_tsquery(?, ?) query' : 'pages', hasQuery ? [dictName, tsq(q)] : [])
.where('siteId', args.siteId)
.where('isSearchableComputed', true)
.where(builder => {
if (args.path) {
builder.where('path', 'ILIKE', `${args.path}%`)
}
if (args.locale?.length > 0) {
builder.whereIn('locale', args.locale)
}
if (args.editor) {
builder.where('editor', args.editor)
}
if (args.publishState) {
builder.where('publishState', args.publishState)
}
if (args.tags) {
builder.where('tags', '@>', args.tags)
}
if (hasQuery) {
builder.whereRaw('query @@ ts')
}
})
.orderBy(args.orderBy || 'relevancy', args.orderByDirection || 'desc')
.offset(args.offset || 0)
.limit(args.limit || 25)
// -> Remove highlights without matches
if (WIKI.config.search.termHighlighting && hasQuery) {
for (const r of results) {
if (r.highlight?.indexOf('<b>') < 0) {
r.highlight = null
}
}
}
return {
results,
totalHits: results?.length > 0 ? results[0].total : 0
}
} catch (err) {
WIKI.logger.warn(`Search Query Error: ${err.message}`)
throw err
}
},
/**
* LIST PAGES
*/
async pages (obj, args, context, info) {
let results = await WIKI.db.pages.query().column([
'pages.id',
'path',
'locale',
'title',
'description',
'isPublished',
'isPrivate',
'privateNS',
'contentType',
'createdAt',
'updatedAt'
])
.withGraphJoined('tags')
.modifyGraph('tags', builder => {
builder.select('tag')
})
.modify(queryBuilder => {
if (args.limit) {
queryBuilder.limit(args.limit)
}
if (args.locale) {
queryBuilder.where('locale', args.locale)
}
if (args.creatorId && args.authorId && args.creatorId > 0 && args.authorId > 0) {
queryBuilder.where(function () {
this.where('creatorId', args.creatorId).orWhere('authorId', args.authorId)
})
} else {
if (args.creatorId && args.creatorId > 0) {
queryBuilder.where('creatorId', args.creatorId)
}
if (args.authorId && args.authorId > 0) {
queryBuilder.where('authorId', args.authorId)
}
}
if (args.tags && args.tags.length > 0) {
queryBuilder.whereIn('tags.tag', args.tags.map(t => _.trim(t).toLowerCase()))
}
const orderDir = args.orderByDirection === 'DESC' ? 'desc' : 'asc'
switch (args.orderBy) {
case 'CREATED':
queryBuilder.orderBy('createdAt', orderDir)
break
case 'PATH':
queryBuilder.orderBy('path', orderDir)
break
case 'TITLE':
queryBuilder.orderBy('title', orderDir)
break
case 'UPDATED':
queryBuilder.orderBy('updatedAt', orderDir)
break
default:
queryBuilder.orderBy('pages.id', orderDir)
break
}
})
results = _.filter(results, r => {
return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
path: r.path,
locale: r.locale
})
}).map(r => ({
...r,
tags: _.map(r.tags, 'tag')
}))
if (args.tags && args.tags.length > 0) {
results = _.filter(results, r => _.every(args.tags, t => _.includes(r.tags, t)))
}
return results
},
/**
* FETCH SINGLE PAGE BY ID
*/
async pageById (obj, args, context, info) {
const page = await WIKI.db.pages.getPageFromDb(args.id)
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
path: page.path,
locale: page.locale
})) {
return {
...page,
...page.config,
scriptCss: page.scripts?.css,
scriptJsLoad: page.scripts?.jsLoad,
scriptJsUnload: page.scripts?.jsUnload
}
} else {
throw new Error('ERR_FORBIDDEN')
}
} else {
throw new Error('ERR_PAGE_NOT_FOUND')
}
},
/**
* FETCH SINGLE PAGE BY PATH
*/
async pageByPath (obj, args, context, info) {
// console.info(info)
const pageArgs = parsePath(args.path)
const page = await WIKI.db.pages.getPageFromDb({
...pageArgs,
siteId: args.siteId
})
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
path: page.path,
locale: page.locale
})) {
return {
...page,
...page.config,
scriptCss: page.scripts?.css,
scriptJsLoad: page.scripts?.jsLoad,
scriptJsUnload: page.scripts?.jsUnload
}
} else {
throw new Error('ERR_FORBIDDEN')
}
} else {
throw new Error('ERR_PAGE_NOT_FOUND')
}
},
/**
* FETCH PATH FROM ALIAS
*/
async pathFromAlias (obj, args, context, info) {
const alias = args.alias?.trim()
if (!alias) {
throw new Error('ERR_PAGE_ALIAS_MISSING')
}
if (!WIKI.sites[args.siteId]) {
throw new Error('ERR_INVALID_SITE')
}
const page = await WIKI.db.pages.query().findOne({
alias: args.alias,
siteId: args.siteId
}).select('id', 'path', 'locale')
if (!page) {
throw new Error('ERR_PAGE_ALIAS_NOT_FOUND')
}
return {
id: page.id,
path: WIKI.sites[args.siteId].config.localeNamespacing ? `${page.locale}/${page.path}` : page.path
}
},
/**
* FETCH TAGS
*/
async tags (obj, args, context, info) {
if (!args.siteId) { throw new Error('Missing Site ID') }
const tags = await WIKI.db.knex('tags').where('siteId', args.siteId).orderBy('tag')
// TODO: check permissions
return tags
},
/**
* FETCH PAGE TREE
*/
async pageTree (obj, args, context, info) {
let curPage = null
if (!args.locale) { args.locale = WIKI.config.lang.code }
if (args.path && !args.parent) {
curPage = await WIKI.db.knex('pageTree').first('parent', 'ancestors').where({
path: args.path,
locale: args.locale
})
if (curPage) {
args.parent = curPage.parent || 0
} else {
return []
}
}
const results = await WIKI.db.knex('pageTree').where(builder => {
builder.where('locale', args.locale)
switch (args.mode) {
case 'FOLDERS':
builder.andWhere('isFolder', true)
break
case 'PAGES':
builder.andWhereNotNull('pageId')
break
}
if (!args.parent || args.parent < 1) {
builder.whereNull('parent')
} else {
builder.where('parent', args.parent)
if (args.includeAncestors && curPage && curPage.ancestors.length > 0) {
builder.orWhereIn('id', _.isString(curPage.ancestors) ? JSON.parse(curPage.ancestors) : curPage.ancestors)
}
}
}).orderBy([{ column: 'isFolder', order: 'desc' }, 'title'])
return results.filter(r => {
return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
path: r.path,
locale: r.locale
})
}).map(r => ({
...r,
parent: r.parent || 0,
locale: r.locale
}))
},
/**
* FETCH PAGE LINKS
*/
async pageLinks (obj, args, context, info) {
let results
if (WIKI.config.db.type === 'mysql' || WIKI.config.db.type === 'mariadb' || WIKI.config.db.type === 'sqlite') {
results = await WIKI.db.knex('pages')
.column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.locale' })
.leftJoin('pageLinks', 'pages.id', 'pageLinks.pageId')
.where({
'pages.locale': args.locale
})
.unionAll(
WIKI.db.knex('pageLinks')
.column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.locale' })
.leftJoin('pages', 'pageLinks.pageId', 'pages.id')
.where({
'pages.locale': args.locale
})
)
} else {
results = await WIKI.db.knex('pages')
.column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.locale' })
.fullOuterJoin('pageLinks', 'pages.id', 'pageLinks.pageId')
.where({
'pages.locale': args.locale
})
}
return _.reduce(results, (result, val) => {
// -> Check if user has access to source and linked page
if (
!WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.path, locale: args.locale }) ||
!WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.link, locale: val.locale })
) {
return result
}
const existingEntry = _.findIndex(result, ['id', val.id])
if (existingEntry >= 0) {
if (val.link) {
result[existingEntry].links.push(`${val.locale}/${val.link}`)
}
} else {
result.push({
id: val.id,
title: val.title,
path: `${args.locale}/${val.path}`,
links: val.link ? [`${val.locale}/${val.link}`] : []
})
}
return result
}, [])
},
/**
* CHECK FOR EDITING CONFLICT
*/
async checkConflicts (obj, args, context, info) {
const page = await WIKI.db.pages.query().select('path', 'locale', 'updatedAt').findById(args.id)
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], {
path: page.path,
locale: page.locale
})) {
return page.updatedAt > args.checkoutDate
} else {
throw new WIKI.Error.PageUpdateForbidden()
}
} else {
throw new WIKI.Error.PageNotFound()
}
},
/**
* FETCH LATEST VERSION FOR CONFLICT COMPARISON
*/
async checkConflictsLatest (obj, args, context, info) {
const page = await WIKI.db.pages.getPageFromDb(args.id)
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], {
path: page.path,
locale: page.locale
})) {
return {
...page,
tags: page.tags.map(t => t.tag),
locale: page.locale
}
} else {
throw new WIKI.Error.PageViewForbidden()
}
} else {
throw new WIKI.Error.PageNotFound()
}
}
},
Mutation: {
/**
* CREATE PAGE
*/
async createPage (obj, args, context) {
try {
const page = await WIKI.db.pages.createPage({
...args,
user: context.req.user
})
return {
operation: generateSuccess('Page created successfully.'),
page
}
} catch (err) {
return generateError(err)
}
},
/**
* UPDATE PAGE
*/
async updatePage (obj, args, context) {
try {
const page = await WIKI.db.pages.updatePage({
...args,
user: context.req.user
})
return {
operation: generateSuccess('Page has been updated.'),
page
}
} catch (err) {
return generateError(err)
}
},
/**
* CONVERT PAGE
*/
async convertPage (obj, args, context) {
try {
await WIKI.db.pages.convertPage({
...args,
user: context.req.user
})
return {
operation: generateSuccess('Page has been converted.')
}
} catch (err) {
return generateError(err)
}
},
/**
* MOVE PAGE
*/
async movePage (obj, args, context) {
try {
await WIKI.db.pages.movePage({
...args,
user: context.req.user
})
return {
operation: generateSuccess('Page has been moved.')
}
} catch (err) {
return generateError(err)
}
},
/**
* DELETE PAGE
*/
async deletePage (obj, args, context) {
try {
await WIKI.db.pages.deletePage({
...args,
user: context.req.user
})
return {
operation: generateSuccess('Page has been deleted.')
}
} catch (err) {
return generateError(err)
}
},
/**
* DELETE TAG
*/
async deleteTag (obj, args, context) {
try {
const tagToDel = await WIKI.db.tags.query().findById(args.id)
if (tagToDel) {
await tagToDel.$relatedQuery('pages').unrelate()
await WIKI.db.tags.query().deleteById(args.id)
} else {
throw new Error('This tag does not exist.')
}
return {
operation: generateSuccess('Tag has been deleted.')
}
} catch (err) {
return generateError(err)
}
},
/**
* UPDATE TAG
*/
async updateTag (obj, args, context) {
try {
const affectedRows = await WIKI.db.tags.query()
.findById(args.id)
.patch({
tag: _.trim(args.tag).toLowerCase(),
title: _.trim(args.title)
})
if (affectedRows < 1) {
throw new Error('This tag does not exist.')
}
return {
operation: generateSuccess('Tag has been updated successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* FLUSH PAGE CACHE
*/
async flushCache (obj, args, context) {
try {
await WIKI.db.pages.flushCache()
WIKI.events.outbound.emit('flushCache')
return {
operation: generateSuccess('Pages Cache has been flushed successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* MIGRATE ALL PAGES FROM SOURCE LOCALE TO TARGET LOCALE
*/
async migrateToLocale (obj, args, context) {
try {
const count = await WIKI.db.pages.migrateToLocale(args)
return {
operation: generateSuccess('Migrated content to target locale successfully.'),
count
}
} catch (err) {
return generateError(err)
}
},
/**
* REBUILD TREE
*/
async rebuildPageTree (obj, args, context) {
try {
await WIKI.db.pages.rebuildTree()
return {
operation: generateSuccess('Page tree rebuilt successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* RERENDER PAGE
*/
async rerenderPage (obj, args, context) {
try {
const page = await WIKI.db.pages.query().findById(args.id)
if (!page) {
throw new WIKI.Error.PageNotFound()
}
await WIKI.db.pages.renderPage(page)
return {
operation: generateSuccess('Page rerendered successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* RESTORE PAGE VERSION
*/
async restorePage (obj, args, context) {
try {
const page = await WIKI.db.pages.query().select('path', 'locale').findById(args.pageId)
if (!page) {
throw new WIKI.Error.PageNotFound()
}
if (!WIKI.auth.checkAccess(context.req.user, ['write:pages'], {
path: page.path,
locale: page.locale
})) {
throw new WIKI.Error.PageRestoreForbidden()
}
const targetVersion = await WIKI.db.pageHistory.getVersion({ pageId: args.pageId, versionId: args.versionId })
if (!targetVersion) {
throw new WIKI.Error.PageNotFound()
}
await WIKI.db.pages.updatePage({
...targetVersion,
id: targetVersion.pageId,
user: context.req.user,
action: 'restored'
})
return {
operation: generateSuccess('Page version restored successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* Purge history
*/
async purgePagesHistory (obj, args, context) {
try {
await WIKI.db.pageHistory.purge(args.olderThan)
return {
operation: generateSuccess('Page history purged successfully.')
}
} catch (err) {
return generateError(err)
}
}
},
Page: {
icon (page) {
return page.icon || 'las la-file-alt'
},
password (page) {
return page.password ? '********' : ''
},
content (page, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:source', 'write:pages', 'manage:pages'], {
path: page.path,
locale: page.locale
})) {
throw new Error('ERR_FORBIDDEN')
}
return page.content
},
// async tags (page) {
// return WIKI.db.pages.relatedQuery('tags').for(page.id)
// },
tocDepth (page) {
return {
min: page.extra?.tocDepth?.min ?? 1,
max: page.extra?.tocDepth?.max ?? 2
}
}
// comments(pg) {
// return pg.$relatedQuery('comments')
// }
}
}

@ -1,52 +0,0 @@
import _ from 'lodash-es'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
export default {
Query: {
async renderers(obj, args, context, info) {
let renderers = await WIKI.db.renderers.getRenderers()
renderers = renderers.map(rdr => {
const rendererInfo = _.find(WIKI.data.renderers, ['key', rdr.key]) || {}
return {
...rendererInfo,
...rdr,
config: _.sortBy(_.transform(rdr.config, (res, value, key) => {
const configData = _.get(rendererInfo.props, key, false)
if (configData) {
res.push({
key,
value: JSON.stringify({
...configData,
value
})
})
}
}, []), 'key')
}
})
// if (args.filter) { renderers = graphHelper.filter(renderers, args.filter) }
if (args.orderBy) { renderers = _.sortBy(renderers, [args.orderBy]) }
return renderers
}
},
Mutation: {
async updateRenderers(obj, args, context) {
try {
for (let rdr of args.renderers) {
await WIKI.db.renderers.query().patch({
isEnabled: rdr.isEnabled,
config: _.reduce(rdr.config, (result, value, key) => {
_.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
return result
}, {})
}).where('key', rdr.key)
}
return {
responseResult: generateSuccess('Renderers updated successfully')
}
} catch (err) {
return generateError(err)
}
}
}
}

@ -1,354 +0,0 @@
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import _ from 'lodash-es'
import CleanCSS from 'clean-css'
import path from 'node:path'
import fs from 'fs-extra'
import { v4 as uuid } from 'uuid'
export default {
Query: {
async sites () {
const sites = await WIKI.db.sites.query().orderBy('hostname')
return sites.map(s => ({
...s.config,
id: s.id,
hostname: s.hostname,
isEnabled: s.isEnabled,
pageExtensions: s.config.pageExtensions.join(', ')
}))
},
async siteById (obj, args) {
const site = await WIKI.db.sites.query().findById(args.id)
return site ? {
...site.config,
id: site.id,
hostname: site.hostname,
isEnabled: site.isEnabled,
pageExtensions: site.config.pageExtensions.join(', ')
} : null
},
async siteByHostname (obj, args) {
let site = await WIKI.db.sites.query().where({
hostname: args.hostname
}).first()
if (!site && !args.exact) {
site = await WIKI.db.sites.query().where({
hostname: '*'
}).first()
}
return site ? {
...site.config,
id: site.id,
hostname: site.hostname,
isEnabled: site.isEnabled,
pageExtensions: site.config.pageExtensions.join(', ')
} : null
}
},
Mutation: {
/**
* CREATE SITE
*/
async createSite (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['write:sites', 'manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Validate inputs
if (!args.hostname || args.hostname.length < 1 || !/^(\\*)|([a-z0-9\-.:]+)$/.test(args.hostname)) {
throw new WIKI.Error.Custom('SiteCreateInvalidHostname', 'Invalid Site Hostname')
}
if (!args.title || args.title.length < 1 || !/^[^<>"]+$/.test(args.title)) {
throw new WIKI.Error.Custom('SiteCreateInvalidTitle', 'Invalid Site Title')
}
// -> Check for duplicate catch-all
if (args.hostname === '*') {
const site = await WIKI.db.sites.query().where({
hostname: args.hostname
}).first()
if (site) {
throw new WIKI.Error.Custom('SiteCreateDuplicateCatchAll', 'A site with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.')
}
}
// -> Create site
const newSite = await WIKI.db.sites.createSite(args.hostname, {
title: args.title
})
return {
operation: generateSuccess('Site created successfully'),
site: newSite
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* UPDATE SITE
*/
async updateSite (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Load site
const site = await WIKI.db.sites.query().findById(args.id)
if (!site) {
throw new WIKI.Error.Custom('SiteInvalidId', 'Invalid Site ID')
}
// -> Check for bad input
if (_.has(args.patch, 'hostname') && _.trim(args.patch.hostname).length < 1) {
throw new WIKI.Error.Custom('SiteInvalidHostname', 'Hostname is invalid.')
}
// -> Check for duplicate catch-all
if (args.patch.hostname === '*' && site.hostname !== '*') {
const dupSite = await WIKI.db.sites.query().where({ hostname: '*' }).first()
if (dupSite) {
throw new WIKI.Error.Custom('SiteUpdateDuplicateCatchAll', `Site ${dupSite.config.title} with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.`)
}
}
// -> Format Code
if (args.patch?.theme?.injectCSS) {
args.patch.theme.injectCSS = new CleanCSS({ inline: false }).minify(args.patch.theme.injectCSS).styles
}
// -> Format Page Extensions
if (args.patch?.pageExtensions) {
args.patch.pageExtensions = args.patch.pageExtensions.split(',').map(ext => ext.trim().toLowerCase()).filter(ext => ext.length > 0)
}
// -> Update site
await WIKI.db.sites.updateSite(args.id, {
hostname: args.patch.hostname ?? site.hostname,
isEnabled: args.patch.isEnabled ?? site.isEnabled,
config: _.defaultsDeep(_.omit(args.patch, ['hostname', 'isEnabled']), site.config)
})
return {
operation: generateSuccess('Site updated successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* DELETE SITE
*/
async deleteSite (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Ensure site isn't last one
const sitesCount = await WIKI.db.sites.query().count('id').first()
if (sitesCount?.count && _.toNumber(sitesCount?.count) <= 1) {
throw new WIKI.Error.Custom('SiteDeleteLastSite', 'Cannot delete the last site. At least 1 site must exists at all times.')
}
// -> Delete site
await WIKI.db.sites.deleteSite(args.id)
return {
operation: generateSuccess('Site deleted successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* UPLOAD LOGO
*/
async uploadSiteLogo (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
const { filename, mimetype, createReadStream } = await args.image
WIKI.logger.info(`Processing site logo ${filename} of type ${mimetype}...`)
if (!WIKI.extensions.ext.sharp.isInstalled) {
throw new Error('This feature requires the Sharp extension but it is not installed.')
}
if (!['.svg', '.png', '.jpg', 'webp', '.gif'].some(s => filename.endsWith(s))) {
throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
}
const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
const destFolder = path.resolve(
process.cwd(),
WIKI.config.dataPath,
`assets`
)
const destPath = path.join(destFolder, `logo-${args.id}.${destFormat}`)
await fs.ensureDir(destFolder)
// -> Resize
await WIKI.extensions.ext.sharp.resize({
format: destFormat,
inputStream: createReadStream(),
outputPath: destPath,
height: 72
})
// -> Save logo meta to DB
const site = await WIKI.db.sites.query().findById(args.id)
if (!site.config.assets.logo) {
site.config.assets.logo = uuid()
}
site.config.assets.logoExt = destFormat
await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
await WIKI.db.sites.reloadCache()
// -> Save image data to DB
const imgBuffer = await fs.readFile(destPath)
await WIKI.db.knex('assets').insert({
id: site.config.assets.logo,
fileName: `_logo.${destFormat}`,
fileExt: `.${destFormat}`,
isSystem: true,
kind: 'image',
mimeType: (destFormat === 'svg') ? 'image/svg' : 'image/png',
fileSize: Math.ceil(imgBuffer.byteLength / 1024),
data: imgBuffer,
authorId: context.req.user.id,
siteId: site.id
}).onConflict('id').merge()
WIKI.logger.info('New site logo processed successfully.')
return {
operation: generateSuccess('Site logo uploaded successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* UPLOAD FAVICON
*/
async uploadSiteFavicon (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
const { filename, mimetype, createReadStream } = await args.image
WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`)
if (!WIKI.extensions.ext.sharp.isInstalled) {
throw new Error('This feature requires the Sharp extension but it is not installed.')
}
if (!['.svg', '.png', '.jpg', '.webp', '.gif'].some(s => filename.endsWith(s))) {
throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
}
const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
const destFolder = path.resolve(
process.cwd(),
WIKI.config.dataPath,
`assets`
)
const destPath = path.join(destFolder, `favicon-${args.id}.${destFormat}`)
await fs.ensureDir(destFolder)
// -> Resize
await WIKI.extensions.ext.sharp.resize({
format: destFormat,
inputStream: createReadStream(),
outputPath: destPath,
width: 64,
height: 64
})
// -> Save favicon meta to DB
const site = await WIKI.db.sites.query().findById(args.id)
if (!site.config.assets.favicon) {
site.config.assets.favicon = uuid()
}
site.config.assets.faviconExt = destFormat
await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
await WIKI.db.sites.reloadCache()
// -> Save image data to DB
const imgBuffer = await fs.readFile(destPath)
await WIKI.db.knex('assets').insert({
id: site.config.assets.favicon,
fileName: `_favicon.${destFormat}`,
fileExt: `.${destFormat}`,
isSystem: true,
kind: 'image',
mimeType: (destFormat === 'svg') ? 'image/svg' : 'image/png',
fileSize: Math.ceil(imgBuffer.byteLength / 1024),
data: imgBuffer,
authorId: context.req.user.id,
siteId: site.id
}).onConflict('id').merge()
WIKI.logger.info('New site favicon processed successfully.')
return {
operation: generateSuccess('Site favicon uploaded successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* UPLOAD LOGIN BG
*/
async uploadSiteLoginBg (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
const { filename, mimetype, createReadStream } = await args.image
WIKI.logger.info(`Processing site login bg ${filename} of type ${mimetype}...`)
if (!WIKI.extensions.ext.sharp.isInstalled) {
throw new Error('This feature requires the Sharp extension but it is not installed.')
}
if (!['.png', '.jpg', '.webp'].some(s => filename.endsWith(s))) {
throw new Error('Invalid File Extension. Must be png, jpg or webp.')
}
const destFolder = path.resolve(
process.cwd(),
WIKI.config.dataPath,
`assets`
)
const destPath = path.join(destFolder, `loginbg-${args.id}.jpg`)
await fs.ensureDir(destFolder)
// -> Resize
await WIKI.extensions.ext.sharp.resize({
format: 'jpg',
inputStream: createReadStream(),
outputPath: destPath,
width: 1920
})
// -> Save login bg meta to DB
const site = await WIKI.db.sites.query().findById(args.id)
if (!site.config.assets.loginBg) {
site.config.assets.loginBg = uuid()
await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
await WIKI.db.sites.reloadCache()
}
// -> Save image data to DB
const imgBuffer = await fs.readFile(destPath)
await WIKI.db.knex('assets').insert({
id: site.config.assets.loginBg,
filename: '_loginbg.jpg',
hash: '_loginbg',
ext: '.jpg',
isSystem: true,
kind: 'image',
mime: 'image/jpg',
fileSize: Math.ceil(imgBuffer.byteLength / 1024),
data: imgBuffer,
authorId: context.req.user.id,
siteId: site.id
}).onConflict('id').merge()
WIKI.logger.info('New site login bg processed successfully.')
return {
operation: generateSuccess('Site login bg uploaded successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
}
},
SiteLocales: {
async active (obj, args, context) {
return obj.active.map(l => WIKI.cache.get(`locale:${l}`))
}
}
}

@ -1,229 +0,0 @@
import _ from 'lodash-es'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { v4 as uuid } from 'uuid'
export default {
Query: {
async storageTargets (obj, args, context, info) {
const dbTargets = await WIKI.db.storage.getTargets({ siteId: args.siteId })
// targets = _.sortBy(targets.map(tgt => {
// const targetInfo = _.find(WIKI.data.storage, ['module', tgt.key]) || {}
// return {
// ...targetInfo,
// ...tgt,
// hasSchedule: (targetInfo.schedule !== false),
// syncInterval: targetInfo.syncInterval || targetInfo.schedule || 'P0D',
// syncIntervalDefault: targetInfo.schedule,
// config: _.sortBy(_.transform(tgt.config, (res, value, key) => {
// const configData = _.get(targetInfo.props, key, false)
// if (configData) {
// res.push({
// key,
// value: JSON.stringify({
// ...configData,
// value: (configData.sensitive && value.length > 0) ? '********' : value
// })
// })
// }
// }, []), 'key')
// }
// }), ['title', 'key'])
return _.sortBy(WIKI.storage.defs.map(md => {
const dbTarget = dbTargets.find(tg => tg.module === md.key)
return {
id: dbTarget?.id ?? uuid(),
isEnabled: dbTarget?.isEnabled ?? false,
module: md.key,
title: md.title,
description: md.description,
icon: md.icon,
banner: md.banner,
vendor: md.vendor,
website: md.website,
contentTypes: {
activeTypes: dbTarget?.contentTypes?.activeTypes ?? md.contentTypes.defaultTypesEnabled,
largeThreshold: dbTarget?.contentTypes?.largeThreshold ?? md.contentTypes.defaultLargeThreshold
},
assetDelivery: {
isStreamingSupported: md?.assetDelivery?.isStreamingSupported ?? false,
isDirectAccessSupported: md?.assetDelivery?.isDirectAccessSupported ?? false,
streaming: dbTarget?.assetDelivery?.streaming ?? md?.assetDelivery?.defaultStreamingEnabled ?? false,
directAccess: dbTarget?.assetDelivery?.directAccess ?? md?.assetDelivery?.defaultDirectAccessEnabled ?? false
},
versioning: {
isSupported: md?.versioning?.isSupported ?? false,
isForceEnabled: md?.versioning?.isForceEnabled ?? false,
enabled: dbTarget?.versioning?.enabled ?? md?.versioning?.defaultEnabled ?? false
},
sync: {},
status: {},
setup: {
handler: md?.setup?.handler,
state: dbTarget?.state?.setup ?? 'notconfigured',
values: md.setup?.handler ?
_.transform(md.setup.defaultValues,
(r, v, k) => {
r[k] = dbTarget?.config?.[k] ?? v
}, {}) :
{}
},
config: _.transform(md.props, (r, v, k) => {
const cfValue = dbTarget?.config?.[k] ?? v.default
r[k] = {
...v,
value: v.sensitive && cfValue ? '********' : cfValue,
...v.enum && {
enum: v.enum.map(o => {
if (o.indexOf('|') > 0) {
const oParsed = o.split('|')
return {
value: oParsed[0],
label: oParsed[1]
}
} else {
return {
value: o,
label: o
}
}
})
}
}
}, {}),
actions: md.actions
}
}), ['title'])
}
},
Mutation: {
async updateStorageTargets (obj, args, context) {
WIKI.logger.debug(`Updating storage targets for site ${args.siteId}...`)
try {
const dbTargets = await WIKI.db.storage.getTargets({ siteId: args.siteId })
for (const tgt of args.targets) {
const md = _.find(WIKI.storage.defs, ['key', tgt.module])
if (!md) {
throw new Error('Invalid module key for non-existent storage target.')
}
const dbTarget = _.find(dbTargets, ['id', tgt.id])
// -> Build update config object
const updatedConfig = dbTarget?.config ?? {}
if (tgt.config) {
for (const [key, prop] of Object.entries(md.props)) {
if (prop.readOnly) { continue }
if (!Object.prototype.hasOwnProperty.call(tgt.config, key)) { continue }
if (prop.sensitive && tgt.config[key] === '********') { continue }
updatedConfig[key] = tgt.config[key]
}
}
// -> Target doesn't exist yet in the DB, let's create it
if (!dbTarget) {
WIKI.logger.debug(`No existing DB configuration for module ${tgt.module}. Creating a new one...`)
await WIKI.db.storage.query().insert({
id: tgt.id,
module: tgt.module,
siteId: args.siteId,
isEnabled: tgt.isEnabled ?? false,
contentTypes: {
activeTypes: tgt.contentTypes ?? md.contentTypes.defaultTypesEnabled ?? [],
largeThreshold: tgt.largeThreshold ?? md.contentTypes.defaultLargeThreshold ?? '5MB'
},
assetDelivery: {
streaming: tgt.assetDeliveryFileStreaming ?? md?.assetDelivery?.defaultStreamingEnabled ?? false,
directAccess: tgt.assetDeliveryDirectAccess ?? md?.assetDelivery?.defaultDirectAccessEnabled ?? false
},
versioning: {
enabled: tgt.useVersioning ?? md?.versioning?.defaultEnabled ?? false
},
state: {
current: 'ok'
},
config: updatedConfig
})
} else {
WIKI.logger.debug(`Updating DB configuration for module ${tgt.module}...`)
await WIKI.db.storage.query().patch({
isEnabled: tgt.isEnabled ?? dbTarget.isEnabled ?? false,
contentTypes: {
activeTypes: tgt.contentTypes ?? dbTarget?.contentTypes?.activeTypes ?? [],
largeThreshold: tgt.largeThreshold ?? dbTarget?.contentTypes?.largeThreshold ?? '5MB'
},
assetDelivery: {
streaming: tgt.assetDeliveryFileStreaming ?? dbTarget?.assetDelivery?.streaming ?? false,
directAccess: tgt.assetDeliveryDirectAccess ?? dbTarget?.assetDelivery?.directAccess ?? false
},
versioning: {
enabled: tgt.useVersioning ?? dbTarget?.versioning?.enabled ?? false
},
config: updatedConfig
}).where('id', tgt.id)
}
}
// await WIKI.db.storage.initTargets()
return {
operation: generateSuccess('Storage targets updated successfully')
}
} catch (err) {
return generateError(err)
}
},
async setupStorageTarget (obj, args, context) {
try {
const tgt = await WIKI.db.storage.query().findById(args.targetId)
if (!tgt) {
throw new Error('Not storage target matching this ID')
}
const md = _.find(WIKI.storage.defs, ['key', tgt.module])
if (!md) {
throw new Error('No matching storage module installed.')
}
if (!await WIKI.db.storage.ensureModule(md.key)) {
throw new Error('Failed to load storage module. Check logs for details.')
}
const result = await WIKI.storage.modules[md.key].setup(args.targetId, args.state)
return {
operation: generateSuccess('Storage target setup step succeeded'),
state: result
}
} catch (err) {
return generateError(err)
}
},
async destroyStorageTargetSetup (obj, args, context) {
try {
const tgt = await WIKI.db.storage.query().findById(args.targetId)
if (!tgt) {
throw new Error('Not storage target matching this ID')
}
const md = _.find(WIKI.storage.defs, ['key', tgt.module])
if (!md) {
throw new Error('No matching storage module installed.')
}
if (!await WIKI.db.storage.ensureModule(md.key)) {
throw new Error('Failed to load storage module. Check logs for details.')
}
await WIKI.storage.modules[md.key].setupDestroy(args.targetId)
return {
operation: generateSuccess('Storage target setup configuration destroyed succesfully.')
}
} catch (err) {
return generateError(err)
}
},
async executeStorageAction (obj, args, context) {
try {
await WIKI.db.storage.executeAction(args.targetKey, args.handler)
return {
operation: generateSuccess('Action completed.')
}
} catch (err) {
return generateError(err)
}
}
}
}

@ -1,475 +0,0 @@
import _ from 'lodash-es'
import util from 'node:util'
import getosSync from 'getos'
import os from 'node:os'
import { filesize } from 'filesize'
import path from 'node:path'
import fs from 'fs-extra'
import { DateTime } from 'luxon'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
const getos = util.promisify(getosSync)
export default {
Query: {
/**
* Metrics Endpoint State
*/
metricsState () {
return WIKI.config.metrics?.isEnabled ?? false
},
/**
* System Flags
*/
systemFlags () {
return WIKI.config.flags
},
/**
* System Info
*/
async systemInfo (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:dashboard', 'manage:sites'])) {
throw new Error('ERR_FORBIDDEN')
}
return {}
},
/**
* System Extensions
*/
async systemExtensions (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled', 'isInstallable']))
for (const ext of exts) {
ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()
}
return exts
},
/**
* List System Instances
*/
async systemInstances (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
const instRaw = await WIKI.db.knex('pg_stat_activity')
.select([
'usename',
'client_addr',
'application_name',
'backend_start',
'state_change'
])
.where('datname', WIKI.db.knex.client.connectionSettings.database)
.andWhereLike('application_name', 'Wiki.js%')
const insts = {}
for (const inst of instRaw) {
const instId = inst.application_name.substring(10, 20)
const conType = inst.application_name.includes(':MAIN') ? 'main' : 'sub'
const curInst = insts[instId] ?? {
activeConnections: 0,
activeListeners: 0,
dbFirstSeen: inst.backend_start,
dbLastSeen: inst.state_change
}
insts[instId] = {
id: instId,
activeConnections: conType === 'main' ? curInst.activeConnections + 1 : curInst.activeConnections,
activeListeners: conType === 'sub' ? curInst.activeListeners + 1 : curInst.activeListeners,
dbUser: inst.usename,
dbFirstSeen: curInst.dbFirstSeen > inst.backend_start ? inst.backend_start : curInst.dbFirstSeen,
dbLastSeen: curInst.dbLastSeen < inst.state_change ? inst.state_change : curInst.dbLastSeen,
ip: inst.client_addr
}
}
return _.values(insts)
},
/**
* System Security Settings
*/
systemSecurity (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.config.security
},
/**
* List System Jobs
*/
async systemJobs (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
const results = args.states?.length > 0
? await WIKI.db.knex('jobHistory').whereIn('state', args.states.map(s => s.toLowerCase())).orderBy('startedAt', 'desc')
: await WIKI.db.knex('jobHistory').orderBy('startedAt', 'desc')
return results.map(r => ({
...r,
state: r.state.toUpperCase()
}))
},
/**
* List Scheduled Jobs
*/
async systemJobsScheduled (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.db.knex('jobSchedule').orderBy('task')
},
/**
* List Upcoming Jobs
*/
async systemJobsUpcoming (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.db.knex('jobs').orderBy([
{ column: 'waitUntil', order: 'asc', nulls: 'first' },
{ column: 'createdAt', order: 'asc' }
])
},
/**
* Search Settings
*/
systemSearch (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
return {
...WIKI.config.search,
dictOverrides: JSON.stringify(WIKI.config.search.dictOverrides, null, 2)
}
}
},
Mutation: {
async cancelJob (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.logger.info(`Admin requested cancelling job ${args.id}...`)
const result = await WIKI.db.knex('jobs')
.where('id', args.id)
.del()
if (result === 1) {
WIKI.logger.info(`Cancelled job ${args.id} [ OK ]`)
} else {
throw new Error('Job has already entered active state or does not exist.')
}
return {
operation: generateSuccess('Cancelled job successfully.')
}
} catch (err) {
if (err.message !== 'ERR_FORBIDDEN') {
WIKI.logger.warn(err)
}
return generateError(err)
}
},
async checkForUpdates (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['read:dashboard', 'manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
const renderJob = await WIKI.scheduler.addJob({
task: 'checkVersion',
maxRetries: 0,
promise: true
})
await renderJob.promise
return {
operation: generateSuccess('Checked for latest version successfully.'),
current: WIKI.version,
latest: WIKI.config.update.version,
latestDate: WIKI.config.update.versionDate
}
} catch (err) {
if (err.message !== 'ERR_FORBIDDEN') {
WIKI.logger.warn(err)
}
return generateError(err)
}
},
async disconnectWS (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.servers.ws.disconnectSockets(true)
WIKI.logger.info('All active websocket connections have been terminated.')
return {
operation: generateSuccess('All websocket connections closed successfully.')
}
} catch (err) {
if (err.message !== 'ERR_FORBIDDEN') {
WIKI.logger.warn(err)
}
return generateError(err)
}
},
async installExtension (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.extensions.ext[args.key].install()
// TODO: broadcast ext install
return {
operation: generateSuccess('Extension installed successfully')
}
} catch (err) {
return generateError(err)
}
},
async rebuildSearchIndex (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.scheduler.addJob({
task: 'rebuildSearchIndex',
maxRetries: 0
})
return {
operation: generateSuccess('Search index rebuild has been scheduled and will start shortly.')
}
} catch (err) {
return generateError(err)
}
},
async retryJob (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.logger.info(`Admin requested rescheduling of job ${args.id}...`)
const job = await WIKI.db.knex('jobHistory')
.where('id', args.id)
.first()
if (!job) {
throw new Error('No such job found.')
} else if (job.state === 'interrupted') {
throw new Error('Cannot reschedule a task that has been interrupted. It will automatically be retried shortly.')
} else if (job.state === 'failed' && job.attempt < job.maxRetries) {
throw new Error('Cannot reschedule a task that has not reached its maximum retry attempts.')
}
await WIKI.db.knex('jobs')
.insert({
id: job.id,
task: job.task,
useWorker: job.useWorker,
payload: job.payload,
retries: job.attempt,
maxRetries: job.maxRetries,
isScheduled: job.wasScheduled,
createdBy: WIKI.INSTANCE_ID
})
WIKI.logger.info(`Job ${args.id} has been rescheduled [ OK ]`)
return {
operation: generateSuccess('Job rescheduled successfully.')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* Set Metrics endpoint state
*/
async setMetricsState (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.metrics.isEnabled = args.enabled
await WIKI.configSvc.saveToDb(['metrics'])
return {
operation: generateSuccess('Metrics endpoint state changed successfully')
}
} catch (err) {
return generateError(err)
}
},
async updateSystemFlags (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.flags = {
...WIKI.config.flags,
...args.flags
}
await WIKI.configSvc.applyFlags()
await WIKI.configSvc.saveToDb(['flags'])
return {
operation: generateSuccess('System Flags applied successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
async updateSystemSearch (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.search = {
...WIKI.config.search,
termHighlighting: args.termHighlighting ?? WIKI.config.search.termHighlighting,
dictOverrides: args.dictOverrides ? JSON.parse(args.dictOverrides) : WIKI.config.search.dictOverrides
}
// TODO: broadcast config update
await WIKI.configSvc.saveToDb(['search'])
return {
operation: generateSuccess('System Search configuration applied successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
async updateSystemSecurity (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.security = _.defaultsDeep(_.omit(args, ['__typename']), WIKI.config.security)
// TODO: broadcast config update
await WIKI.configSvc.saveToDb(['security'])
return {
operation: generateSuccess('System Security configuration applied successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
}
},
SystemInfo: {
configFile () {
return path.join(process.cwd(), 'config.yml')
},
cpuCores () {
return os.cpus().length
},
currentVersion () {
return WIKI.version
},
dbHost () {
return WIKI.config.db.host
},
dbVersion () {
return WIKI.db.VERSION
},
hostname () {
return os.hostname()
},
httpPort () {
return WIKI.servers.servers.http ? _.get(WIKI.servers.servers.http.address(), 'port', 0) : 0
},
httpRedirection () {
return _.get(WIKI.config, 'server.sslRedir', false)
},
httpsPort () {
return WIKI.servers.servers.https ? _.get(WIKI.servers.servers.https.address(), 'port', 0) : 0
},
isMailConfigured () {
return WIKI.config?.mail?.host?.length > 2
},
async isSchedulerHealthy () {
const results = await WIKI.db.knex('jobHistory').count('* as total').whereIn('state', ['failed', 'interrupted']).andWhere('startedAt', '>=', DateTime.utc().minus({ days: 1 }).toISO()).first()
return _.toSafeInteger(results?.total) === 0
},
latestVersion () {
return WIKI.config.update.version
},
latestVersionReleaseDate () {
return DateTime.fromISO(WIKI.config.update.versionDate).toJSDate()
},
nodeVersion () {
return process.version.substring(1)
},
async operatingSystem () {
let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`
if (os.platform() === 'linux') {
const osInfo = await getos()
osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`
}
return osLabel
},
async platform () {
const isDockerized = await fs.pathExists('/.dockerenv')
if (isDockerized) {
return 'docker'
}
return os.platform()
},
ramTotal () {
return filesize(os.totalmem())
},
sslDomain () {
return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? WIKI.config.ssl.domain : null
},
sslExpirationDate () {
return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? _.get(WIKI.config.letsencrypt, 'payload.expires', null) : null
},
sslProvider () {
return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null
},
sslStatus () {
return 'OK'
},
sslSubscriberEmail () {
return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? WIKI.config.ssl.subscriberEmail : null
},
async upgradeCapable () {
return !_.isNil(process.env.UPGRADE_COMPANION)
},
workingDirectory () {
return process.cwd()
},
async groupsTotal () {
const total = await WIKI.db.groups.query().count('* as total').first()
return _.toSafeInteger(total.total)
},
async pagesTotal () {
const total = await WIKI.db.pages.query().count('* as total').first()
return _.toSafeInteger(total.total)
},
async tagsTotal () {
const total = await WIKI.db.tags.query().count('* as total').first()
return _.toSafeInteger(total.total)
},
async usersTotal () {
const total = await WIKI.db.users.query().count('* as total').first()
return _.toSafeInteger(total.total)
},
async loginsPastDay () {
const total = await WIKI.db.users.query().count('* as total').whereRaw('"lastLoginAt" >= NOW() - INTERVAL \'1 DAY\'').first()
return _.toSafeInteger(total.total)
}
}
}

@ -1,221 +0,0 @@
import _ from 'lodash-es'
import {
decodeTreePath,
encodeTreePath
} from '../../helpers/common.mjs'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
const typeResolvers = {
folder: 'TreeItemFolder',
page: 'TreeItemPage',
asset: 'TreeItemAsset'
}
const rePathName = /^[a-z0-9-]+$/
const reTitle = /^[^<>"]+$/
export default {
Query: {
/**
* FETCH TREE
*/
async tree (obj, args, context, info) {
// Offset
const offset = args.offset || 0
if (offset < 0) {
throw new Error('Invalid Offset')
}
// Limit
const limit = args.limit || 1000
if (limit < 1 || limit > 1000) {
throw new Error('Invalid Limit')
}
// Order By
const orderByDirection = args.orderByDirection || 'asc'
const orderBy = args.orderBy || 'title'
// Parse depth
const depth = args.depth || 0
if (depth < 0 || depth > 10) {
throw new Error('Invalid Depth')
}
const depthCondition = depth > 0 ? `*{,${depth}}` : '*{0}'
// Get parent path
let parentPath = ''
if (args.parentId) {
const parent = await WIKI.db.knex('tree').where('id', args.parentId).first()
if (parent) {
parentPath = (parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName)
}
} else if (args.parentPath) {
parentPath = encodeTreePath(args.parentPath)
}
const folderPathCondition = parentPath ? `${parentPath}.${depthCondition}` : depthCondition
// Fetch Items
const items = await WIKI.db.knex('tree')
.select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
.where(builder => {
builder.where('folderPath', '~', folderPathCondition)
// -> Include ancestors
if (args.includeAncestors) {
const parentPathParts = parentPath.split('.')
for (let i = 0; i <= parentPathParts.length; i++) {
builder.orWhere({
folderPath: _.dropRight(parentPathParts, i).join('.'),
fileName: _.nth(parentPathParts, i * -1),
type: 'folder'
})
}
}
// -> Include root items
if (args.includeRootFolders) {
builder.orWhere({
folderPath: '',
type: 'folder'
})
}
// -> Filter by tags
if (args.tags && args.tags.length > 0) {
builder.where('tags', '@>', args.tags)
}
})
.andWhere(builder => {
// -> Limit to specific types
if (args.types && args.types.length > 0) {
builder.whereIn('type', args.types)
}
})
.limit(limit)
.offset(offset)
.orderBy([
{ column: 'depth' },
{ column: orderBy, order: orderByDirection }
])
return items.map(item => ({
id: item.id,
depth: item.depth,
type: item.type,
folderPath: decodeTreePath(item.folderPath),
fileName: item.fileName,
title: item.title,
tags: item.tags ?? [],
createdAt: item.createdAt,
updatedAt: item.updatedAt,
...(item.type === 'folder') && {
childrenCount: item.meta?.children || 0,
isAncestor: item.folderPath.length < parentPath.length
},
...(item.type === 'asset') && {
fileSize: item.meta?.fileSize || 0,
fileExt: item.meta?.fileExt || '',
mimeType: item.meta?.mimeType || ''
},
...(item.type === 'page') && {
description: item.meta?.description || ''
}
}))
},
/**
* FETCH SINGLE FOLDER BY ID
*/
async folderById (obj, args, context) {
const folder = await WIKI.db.knex('tree')
.select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
.where('id', args.id)
.first()
if (!folder) {
throw new Error('ERR_INVALID_FOLDER')
}
return {
...folder,
folderPath: folder.folderPath.replaceAll('.', '/').replaceAll('_', '-'),
childrenCount: folder.meta?.children || 0
}
},
/**
* FETCH SINGLE FOLDER BY PATH
*/
async folderByPath (obj, args, context) {
const parentPathParts = args.path.replaceAll('/', '.').replaceAll('-', '_').split('.')
const folder = await WIKI.db.knex('tree')
.select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
.where({
siteId: args.siteId,
locale: args.locale,
folderPath: _.dropRight(parentPathParts).join('.'),
fileName: _.last(parentPathParts)
})
.first()
if (!folder) {
throw new Error('ERR_INVALID_FOLDER')
}
return {
...folder,
folderPath: folder.folderPath.replaceAll('.', '/').replaceAll('_', '-'),
childrenCount: folder.meta?.children || 0
}
}
},
Mutation: {
/**
* CREATE FOLDER
*/
async createFolder (obj, args, context) {
try {
await WIKI.db.tree.createFolder(args)
return {
operation: generateSuccess('Folder created successfully')
}
} catch (err) {
WIKI.logger.debug(`Failed to create folder: ${err.message}`)
return generateError(err)
}
},
/**
* RENAME FOLDER
*/
async renameFolder (obj, args, context) {
try {
await WIKI.db.tree.renameFolder(args)
return {
operation: generateSuccess('Folder renamed successfully')
}
} catch (err) {
WIKI.logger.debug(`Failed to rename folder ${args.folderId}: ${err.message}`)
return generateError(err)
}
},
/**
* DELETE FOLDER
*/
async deleteFolder (obj, args, context) {
try {
await WIKI.db.tree.deleteFolder(args.folderId)
return {
operation: generateSuccess('Folder deleted successfully')
}
} catch (err) {
WIKI.logger.debug(`Failed to delete folder ${args.folderId}: ${err.message}`)
return generateError(err)
}
}
},
TreeItem: {
__resolveType (obj, context, info) {
return typeResolvers[obj.type] ?? null
}
}
}

@ -1,475 +0,0 @@
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { find, isNil, mapValues, transform } from 'lodash-es'
import path from 'node:path'
import fs from 'fs-extra'
import { DateTime } from 'luxon'
import bcrypt from 'bcryptjs'
export default {
Query: {
/**
* FETCH ALL USERS
*/
async users (obj, args, context, info) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:users', 'write:users', 'manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
// -> Sanitize limit
let limit = args.pageSize ?? 20
if (limit < 1 || limit > 1000) {
limit = 1000
}
// -> Sanitize offset
let offset = args.page ?? 1
if (offset < 1) {
offset = 1
}
// -> Fetch Users
const total = await WIKI.db.users.query().count('id').first()
const users = await WIKI.db.users.query()
.select('id', 'email', 'name', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt')
.where(builder => {
if (args.filter) {
builder.where('email', 'like', `%${args.filter}%`)
.orWhere('name', 'like', `%${args.filter}%`)
}
})
.orderBy(args.orderBy ?? 'name', args.orderByDirection ?? 'asc')
.offset((offset - 1) * limit)
.limit(limit)
return {
users,
total: total.count
}
},
/**
* FETCH A SINGLE USER
*/
async userById (obj, args, context, info) {
if (!context.req.isAuthenticated || context.req.user.id !== args.id) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:users', 'write:users', 'manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
}
const usr = await WIKI.db.users.query().findById(args.id)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
// const str = _.get(WIKI.auth.strategies, usr.providerKey)
// str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])
// usr.providerName = str.displayName
// usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false)
usr.auth = mapValues(usr.auth, (auth, providerKey) => {
if (auth.password) {
auth.password = 'redacted'
}
if (auth.tfaSecret) {
auth.tfaSecret = 'redacted'
}
return auth
})
usr.passkeys = usr.passkeys.authenticators?.map(a => ({
id: a.id,
createdAt: DateTime.fromISO(a.createdAt).toJSDate(),
name: a.name,
siteHostname: a.rpID
})) ?? []
return usr
},
async userDefaults (obj, args, context) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:users', 'write:users', 'manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.config.userDefaults
},
async userEditorSettings (obj, args, context) {
if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
throw new WIKI.Error.AuthRequired()
}
const config = await WIKI.db.knex('userEditorSettings').first('config').where({
id: context.req.user.id,
editor: args.editor
})
return config ?? {}
},
async lastLogins (obj, args, context, info) {
if (!WIKI.auth.checkAccess(context.req.user, ['read:dashboard', 'read:users', 'write:users', 'manage:users'])) {
throw new Error('ERR_FORBIDDEN')
}
return WIKI.db.users.query()
.select('id', 'name', 'lastLoginAt')
.whereNotNull('lastLoginAt')
.orderBy('lastLoginAt', 'desc')
.limit(10)
},
async userPermissions (obj, args, context) {
if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
throw new WIKI.Error.AuthRequired()
}
const currentUser = await WIKI.db.users.getById(context.req.user.id)
return currentUser.getPermissions()
},
async userPermissionsAtPath (obj, args, context) {
return []
}
},
Mutation: {
async createUser (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['write:users', 'manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.users.createNewUser({ ...args, isVerified: true })
return {
operation: generateSuccess('User created successfully')
}
} catch (err) {
return generateError(err)
}
},
async deleteUser (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
if (args.id === WIKI.auth.guest.id) {
throw new WIKI.Error.UserDeleteProtected()
}
await WIKI.db.users.deleteUser(args.id, args.replaceId)
WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' })
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
return {
operation: generateSuccess('User deleted successfully')
}
} catch (err) {
return generateError(err)
}
},
async updateUser (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.users.updateUser(args.id, args.patch)
return {
operation: generateSuccess('User updated successfully')
}
} catch (err) {
return generateError(err)
}
},
async verifyUser (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.users.query().patch({ isVerified: true }).findById(args.id)
return {
operation: generateSuccess('User verified successfully')
}
} catch (err) {
return generateError(err)
}
},
async activateUser (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.users.query().patch({ isActive: true }).findById(args.id)
return {
operation: generateSuccess('User activated successfully')
}
} catch (err) {
return generateError(err)
}
},
async deactivateUser (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
if (args.id <= 2) {
throw new Error('Cannot deactivate system accounts.')
}
await WIKI.db.users.query().patch({ isActive: false }).findById(args.id)
WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' })
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
return {
operation: generateSuccess('User deactivated successfully')
}
} catch (err) {
return generateError(err)
}
},
async enableUserTFA (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
return {
operation: generateSuccess('User 2FA enabled successfully')
}
} catch (err) {
return generateError(err)
}
},
async disableUserTFA (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
await WIKI.db.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
return {
operation: generateSuccess('User 2FA disabled successfully')
}
} catch (err) {
return generateError(err)
}
},
async changeUserPassword (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
if (args.newPassword?.length < 8) {
throw new Error('ERR_PASSWORD_TOO_SHORT')
}
const usr = await WIKI.db.users.query().findById(args.id)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
const localAuth = await WIKI.db.authentication.getStrategy('local')
usr.auth[localAuth.id].password = await bcrypt.hash(args.newPassword, 12)
if (!isNil(args.mustChangePassword)) {
usr.auth[localAuth.id].mustChangePwd = args.mustChangePassword
}
await WIKI.db.users.query().patch({
auth: usr.auth
}).findById(args.id)
return {
operation: generateSuccess('User password updated successfully')
}
} catch (err) {
return generateError(err)
}
},
async updateProfile (obj, args, context) {
try {
if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(context.req.user.id)
if (!usr.isActive) {
throw new Error('ERR_INACTIVE_USER')
}
if (!usr.isVerified) {
throw new Error('ERR_USER_NOT_VERIFIED')
}
if (args.dateFormat && !['', 'DD/MM/YYYY', 'DD.MM.YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'].includes(args.dateFormat)) {
throw new Error('ERR_INVALID_INPUT')
}
if (args.appearance && !['site', 'light', 'dark'].includes(args.appearance)) {
throw new Error('ERR_INVALID_INPUT')
}
await WIKI.db.users.query().findById(usr.id).patch({
name: args.name?.trim() ?? usr.name,
meta: {
...usr.meta,
location: args.location?.trim() ?? usr.meta.location,
jobTitle: args.jobTitle?.trim() ?? usr.meta.jobTitle,
pronouns: args.pronouns?.trim() ?? usr.meta.pronouns
},
prefs: {
...usr.prefs,
timezone: args.timezone || usr.prefs.timezone,
dateFormat: args.dateFormat ?? usr.prefs.dateFormat,
timeFormat: args.timeFormat ?? usr.prefs.timeFormat,
appearance: args.appearance || usr.prefs.appearance,
cvd: args.cvd || usr.prefs.cvd
}
})
return {
operation: generateSuccess('User profile updated successfully')
}
} catch (err) {
return generateError(err)
}
},
/**
* UPLOAD USER AVATAR
*/
async uploadUserAvatar (obj, args, context) {
try {
if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(context.req.user.id)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
const { filename, mimetype, createReadStream } = await args.image
const lowercaseFilename = filename.toLowerCase()
WIKI.logger.debug(`Processing user ${args.id} avatar ${lowercaseFilename} of type ${mimetype}...`)
if (!WIKI.extensions.ext.sharp.isInstalled) {
throw new Error('This feature requires the Sharp extension but it is not installed. Contact your wiki administrator.')
}
if (!['.png', '.jpg', '.webp', '.gif'].some(s => lowercaseFilename.endsWith(s))) {
throw new Error('Invalid File Extension. Must be png, jpg, webp or gif.')
}
const destFolder = path.resolve(
process.cwd(),
WIKI.config.dataPath,
'assets'
)
const destPath = path.join(destFolder, `userav-${args.id}.jpg`)
await fs.ensureDir(destFolder)
// -> Resize
await WIKI.extensions.ext.sharp.resize({
format: 'jpg',
inputStream: createReadStream(),
outputPath: destPath,
width: 180,
height: 180
})
// -> Set avatar flag for this user in the DB
usr.$query().patch({ hasAvatar: true })
// -> Save image data to DB
const imgBuffer = await fs.readFile(destPath)
await WIKI.db.knex('userAvatars').insert({
id: args.id,
data: imgBuffer
}).onConflict('id').merge()
WIKI.logger.debug(`Processed user ${args.id} avatar successfully.`)
return {
operation: generateSuccess('User avatar uploaded successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* CLEAR USER AVATAR
*/
async clearUserAvatar (obj, args, context) {
try {
if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
throw new Error('ERR_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(context.req.user.id)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
WIKI.logger.debug(`Clearing user ${args.id} avatar...`)
usr.$query.patch({ hasAvatar: false })
await WIKI.db.knex('userAvatars').where({ id: args.id }).del()
WIKI.logger.debug(`Cleared user ${args.id} avatar successfully.`)
return {
operation: generateSuccess('User avatar cleared successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
/**
* UPDATE USER DEFAULTS
*/
async updateUserDefaults (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users', 'manage:groups'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.userDefaults = {
timezone: args.timezone,
dateFormat: args.dateFormat,
timeFormat: args.timeFormat
}
await WIKI.configSvc.saveToDb(['userDefaults'])
return {
operation: generateSuccess('User defaults saved successfully')
}
} catch (err) {
return generateError(err)
}
}
},
User: {
async auth (usr, args, context) {
const authStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
return transform(usr.auth, (result, value, key) => {
const authStrategy = find(authStrategies, ['id', key])
const authModule = find(WIKI.data.authentication, ['key', authStrategy.module])
if (!authStrategy || !authModule) { return }
result.push({
authId: key,
authName: authStrategy.displayName,
strategyKey: authStrategy.module,
strategyIcon: authModule.icon,
config: authStrategy.module === 'local'
? {
isPasswordSet: value.password?.length > 0,
isTfaSetup: value.tfaIsActive && value.tfaSecret?.length > 0,
isTfaRequired: (value.tfaRequired || authStrategy.config.enforceTfa) ?? false,
mustChangePwd: value.mustChangePwd ?? false,
restrictLogin: value.restrictLogin ?? false
}
: value
})
}, [])
},
groups (usr) {
return usr.$relatedQuery('groups')
}
}
}

@ -1,27 +0,0 @@
import gql from 'graphql'
import { DateTime } from 'luxon'
function parseDateTime (value) {
const nDate = DateTime.fromISO(value)
return nDate.isValid ? nDate : null
}
export default new gql.GraphQLScalarType({
name: 'Date',
description: 'ISO date-time string at UTC',
parseValue(value) {
if (typeof value !== 'string') {
throw new TypeError('Date value must be an string!')
}
return parseDateTime(value)
},
serialize(value) {
return value.toISOString()
},
parseLiteral(ast) {
if (ast.kind !== gql.Kind.STRING) {
throw new TypeError('Date value must be an string!')
}
return parseDateTime(ast.value)
}
})

@ -1,57 +0,0 @@
import { Kind, GraphQLScalarType } from 'graphql'
function ensureObject (value) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new TypeError(`JSONObject cannot represent non-object value: ${value}`)
}
return value
}
function parseLiteral (typeName, ast, variables) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value)
case Kind.OBJECT:
return parseObject(typeName, ast, variables)
case Kind.LIST:
return ast.values.map((n) => parseLiteral(typeName, n, variables))
case Kind.NULL:
return null
case Kind.VARIABLE:
return variables ? variables[ast.name.value] : undefined
default:
throw new TypeError(`${typeName} cannot represent value: ${ast}`)
}
}
function parseObject (typeName, ast, variables) {
const value = Object.create(null)
ast.fields.forEach((field) => {
// eslint-disable-next-line no-use-before-define
value[field.name.value] = parseLiteral(typeName, field.value, variables)
})
return value
}
export default new GraphQLScalarType({
name: 'JSON',
description:
'The `JSON` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
specifiedByUrl:
'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
serialize: ensureObject,
parseValue: ensureObject,
parseLiteral: (ast, variables) => {
if (ast.kind !== Kind.OBJECT) {
throw new TypeError(`JSONObject cannot represent non-object value: ${ast}`)
}
return parseObject('JSONObject', ast, variables)
}
})

@ -1,36 +0,0 @@
import { Kind, GraphQLScalarType } from 'graphql'
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
const nilUUID = '00000000-0000-0000-0000-000000000000'
function isUUID (value) {
return uuidRegex.test(value) || nilUUID === value
}
export default new GraphQLScalarType({
name: 'UUID',
description: 'The `UUID` scalar type represents UUID values as specified by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).',
serialize: (value) => {
if (!isUUID(value)) {
throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
}
return value.toLowerCase()
},
parseValue: (value) => {
if (!isUUID(value)) {
throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
}
return value.toLowerCase()
},
parseLiteral: (ast) => {
if (ast.kind === Kind.STRING) {
if (isUUID(ast.value)) {
return ast.value
}
}
return undefined
}
})

@ -1,73 +0,0 @@
# ===============================================
# ANALYTICS
# ===============================================
extend type Query {
"""
Fetch list of Analytics providers and their configuration
"""
analyticsProviders(
"Return only active providers"
isEnabled: Boolean
): [AnalyticsProvider]
}
extend type Mutation {
"""
Update a list of Analytics providers and their configuration
"""
updateAnalyticsProviders(
"List of providers"
providers: [AnalyticsProviderInput]!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
"""
Analytics Provider
"""
type AnalyticsProvider {
"Is the provider active"
isEnabled: Boolean!
"Unique identifier for this provider"
key: String!
"List of configuration properties, formatted as stringified JSON objects"
props: [String]
"Name of the provider"
title: String!
"Short description of the provider"
description: String
"Is the provider available for use"
isAvailable: Boolean
"Path to the provider logo"
logo: String
"Website of the provider"
website: String
"Configuration values for this provider"
config: [KeyValuePair]
}
"""
Analytics Configuration Input
"""
input AnalyticsProviderInput {
"Is the provider active"
isEnabled: Boolean!
"Unique identifier of the provider"
key: String!
"Configuration values for this provider"
config: [KeyValuePairInput]
}

@ -1,57 +0,0 @@
# ===============================================
# ASSETS
# ===============================================
extend type Query {
assetById(
id: UUID!
): AssetItem
}
extend type Mutation {
renameAsset(
id: UUID!
fileName: String!
): DefaultResponse
deleteAsset(
id: UUID!
): DefaultResponse
"""
Upload one or more assets.
Must provide either `folderId` or a combination of `folderPath`, `locale` and `siteId`.
"""
uploadAssets(
folderId: UUID
folderPath: String
locale: String
siteId: UUID
files: [Upload!]!
): DefaultResponse
flushTempUploads: DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type AssetItem {
id: UUID
fileName: String
ext: String
kind: AssetKind
mime: String
fileSize: Int
metadata: JSON
createdAt: Date
updatedAt: Date
author: User
}
enum AssetKind {
document
image
other
}

@ -1,213 +0,0 @@
# ===============================================
# AUTHENTICATION
# ===============================================
extend type Query {
apiKeys: [AuthenticationApiKey]
apiState: Boolean
authStrategies: [AuthenticationStrategy]
authActiveStrategies(
enabledOnly: Boolean
): [AuthenticationActiveStrategy]
authSiteStrategies(
siteId: UUID!
visibleOnly: Boolean
): [AuthenticationSiteStrategy]
}
extend type Mutation {
createApiKey(
name: String!
expiration: String!
groups: [UUID]!
): AuthenticationCreateApiKeyResponse
login(
username: String!
password: String!
strategyId: UUID!
siteId: UUID!
): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
loginTFA(
continuationToken: String!
securityCode: String!
strategyId: UUID!
siteId: UUID!
setup: Boolean
): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
setupTFA(
strategyId: UUID!
siteId: UUID!
): AuthenticationSetupTFAResponse
deactivateTFA(
strategyId: UUID!
): DefaultResponse
setupPasskey(
siteId: UUID!
): AuthenticationSetupPasskeyResponse
finalizePasskey(
registrationResponse: JSON!
name: String!
): DefaultResponse
deactivatePasskey(
id: UUID!
): DefaultResponse
authenticatePasskeyGenerate(
email: String!
siteId: UUID!
): AuthenticationPasskeyResponse @rateLimit(limit: 5, duration: 60)
authenticatePasskeyVerify(
authResponse: JSON!
): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
changePassword(
continuationToken: String
currentPassword: String
newPassword: String!
strategyId: UUID!
siteId: UUID!
): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
forgotPassword(
email: String!
): DefaultResponse @rateLimit(limit: 3, duration: 60)
register(
email: String!
password: String!
name: String!
): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
refreshToken(
token: String!
): AuthenticationTokenResponse @rateLimit(limit: 30, duration: 60)
revokeApiKey(
id: UUID!
): DefaultResponse
setApiState(
enabled: Boolean!
): DefaultResponse
updateAuthStrategies(
strategies: [AuthenticationStrategyInput]!
): DefaultResponse
regenerateCertificates: DefaultResponse
resetGuestUser: DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type AuthenticationStrategy {
key: String
props: JSON
refs: JSON
title: String
description: String
isAvailable: Boolean
useForm: Boolean
usernameType: String
logo: String
color: String
vendor: String
website: String
icon: String
}
type AuthenticationActiveStrategy {
id: UUID
strategy: AuthenticationStrategy
displayName: String
isEnabled: Boolean
config: JSON
registration: Boolean
allowedEmailRegex: String
autoEnrollGroups: [UUID]
}
type AuthenticationSiteStrategy {
id: UUID
activeStrategy: AuthenticationActiveStrategy
isVisible: Boolean
}
type AuthenticationAuthResponse {
operation: Operation
jwt: String
nextAction: AuthenticationNextAction
continuationToken: String
redirect: String
tfaQRImage: String
}
type AuthenticationTokenResponse {
operation: Operation
jwt: String
}
type AuthenticationSetupTFAResponse {
operation: Operation
continuationToken: String
tfaQRImage: String
}
type AuthenticationSetupPasskeyResponse {
operation: Operation
registrationOptions: JSON
}
type AuthenticationPasskeyResponse {
operation: Operation
authOptions: JSON
}
input AuthenticationStrategyInput {
key: String!
strategyKey: String!
config: JSON!
displayName: String!
order: Int!
isEnabled: Boolean!
registration: Boolean!
allowedEmailRegex: String!
autoEnrollGroups: [UUID]!
}
type AuthenticationApiKey {
id: UUID
name: String
keyShort: String
expiration: Date
createdAt: Date
updatedAt: Date
isRevoked: Boolean
}
type AuthenticationCreateApiKeyResponse {
operation: Operation
key: String
}
enum AuthenticationNextAction {
changePassword
setupTfa
provideTfa
redirect
}

@ -1,40 +0,0 @@
# ===============================================
# BLOCKS
# ===============================================
extend type Query {
blocks(
siteId: UUID!
): [Block]
}
extend type Mutation {
setBlocksState(
siteId: UUID!
states: [BlockStateInput]!
): DefaultResponse
deleteBlock(
id: UUID!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type Block {
id: UUID
block: String
name: String
description: String
icon: String
isEnabled: Boolean
isCustom: Boolean
config: JSON
}
input BlockStateInput {
id: UUID!
isEnabled: Boolean!
}

@ -1,82 +0,0 @@
# ===============================================
# COMMENT
# ===============================================
extend type Query {
commentsProviders: [CommentProvider]
comments(
locale: String!
path: String!
): [CommentPost]!
commentById(
id: Int!
): CommentPost
}
extend type Mutation {
updateCommentsProviders(
providers: [CommentProviderInput]
): DefaultResponse
createComment(
pageId: Int!
replyTo: Int
content: String!
guestName: String
guestEmail: String
): CommentCreateResponse @rateLimit(limit: 1, duration: 15)
updateComment(
id: Int!
content: String!
): CommentUpdateResponse
deleteComment(
id: Int!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type CommentProvider {
isEnabled: Boolean
key: String
title: String
description: String
logo: String
website: String
isAvailable: Boolean
config: [KeyValuePair]
}
input CommentProviderInput {
isEnabled: Boolean!
key: String!
config: [KeyValuePairInput]
}
type CommentPost {
id: Int
content: String
render: String
authorId: Int
authorName: String
authorEmail: String
authorIP: String
createdAt: Date
updatedAt: Date
}
type CommentCreateResponse {
operation: Operation
id: Int
}
type CommentUpdateResponse {
operation: Operation
render: String
}

@ -1,48 +0,0 @@
# ====================== #
# Wiki.js GraphQL Schema #
# ====================== #
# DIRECTIVES (deprecated)
# ----------
directive @auth(requires: [String]) on QUERY | FIELD_DEFINITION | ARGUMENT_DEFINITION
# TYPES
# -----
# Generic Key Value Pair
type KeyValuePair {
key: String
value: String
}
# General Key Value Pair Input
input KeyValuePairInput {
key: String!
value: String!
}
# Generic Mutation Response
type DefaultResponse {
operation: Operation
}
# Mutation Operation
type Operation {
succeeded: Boolean
slug: String
message: String
}
enum OrderByDirection {
asc
desc
}
# ROOT
# ----
# Query (Read)
type Query
# Mutations (Create, Update, Delete)
type Mutation

@ -1,110 +0,0 @@
# ===============================================
# GROUPS
# ===============================================
extend type Query {
groups(
filter: String
orderBy: String
): [Group]
groupById(
id: UUID!
): Group
}
extend type Mutation {
createGroup(
name: String!
): GroupResponse
updateGroup(
id: UUID!
name: String!
redirectOnLogin: String!
permissions: [String]!
rules: [GroupRuleInput]!
): DefaultResponse
deleteGroup(
id: UUID!
): DefaultResponse
assignUserToGroup(
groupId: UUID!
userId: UUID!
): DefaultResponse
unassignUserFromGroup(
groupId: UUID!
userId: UUID!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type GroupResponse {
operation: Operation
group: Group
}
type Group {
id: UUID
name: String
isSystem: Boolean
redirectOnLogin: String
redirectOnFirstLogin: String
redirectOnLogout: String
permissions: [String]
rules: [GroupRule]
users(
page: Int
pageSize: Int
orderBy: UserOrderBy
orderByDirection: OrderByDirection
# Filter by name / email
filter: String
): [UserMinimal]
userCount: Int
createdAt: Date
updatedAt: Date
}
type GroupRule {
id: UUID
name: String
mode: GroupRuleMode
match: GroupRuleMatch
roles: [String]
path: String
locales: [String]
sites: [UUID]
}
input GroupRuleInput {
id: UUID!
name: String!
mode: GroupRuleMode!
match: GroupRuleMatch!
roles: [String]!
path: String!
locales: [String]!
sites: [UUID]
}
enum GroupRuleMode {
ALLOW
DENY
FORCEALLOW
}
enum GroupRuleMatch {
START
EXACT
END
REGEX
TAG
TAGALL
}

@ -1,70 +0,0 @@
# ===============================================
# WEBHOOKS
# ===============================================
extend type Query {
hooks: [Hook]
hookById(
id: UUID!
): Hook
}
extend type Mutation {
createHook(
name: String!
events: [String]!
url: String!
includeMetadata: Boolean!
includeContent: Boolean!
acceptUntrusted: Boolean!
authHeader: String
): HookCreateResponse
updateHook(
id: UUID!
patch: HookUpdateInput!
): DefaultResponse
deleteHook (
id: UUID!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type Hook {
id: UUID
name: String
events: [String]
url: String
includeMetadata: Boolean
includeContent: Boolean
acceptUntrusted: Boolean
authHeader: String
state: HookState
lastErrorMessage: String
}
input HookUpdateInput {
name: String
events: [String]
url: String
includeMetadata: Boolean
includeContent: Boolean
acceptUntrusted: Boolean
authHeader: String
}
enum HookState {
pending
error
success
}
type HookCreateResponse {
operation: Operation
hook: Hook
}

@ -1,25 +0,0 @@
# ===============================================
# LOCALIZATION
# ===============================================
extend type Query {
locales: [LocalizationLocale]
localeStrings(locale: String!): JSON
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type LocalizationLocale {
completeness: Int
code: String
createdAt: Date
isRTL: Boolean
language: String
name: String
nativeName: String
region: String
script: String
updatedAt: Date
}

@ -1,51 +0,0 @@
# ===============================================
# MAIL
# ===============================================
extend type Query {
mailConfig: MailConfig
}
extend type Mutation {
sendMailTest(
recipientEmail: String!
): DefaultResponse
updateMailConfig(
senderName: String!
senderEmail: String!
defaultBaseURL: String!
host: String!
port: Int!
name: String!
secure: Boolean!
verifySSL: Boolean!
user: String!
pass: String!
useDKIM: Boolean!
dkimDomainName: String!
dkimKeySelector: String!
dkimPrivateKey: String!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type MailConfig {
senderName: String
senderEmail: String
defaultBaseURL: String
host: String
port: Int
name: String
secure: Boolean
verifySSL: Boolean
user: String
pass: String
useDKIM: Boolean
dkimDomainName: String
dkimKeySelector: String
dkimPrivateKey: String
}

@ -1,67 +0,0 @@
# ===============================================
# NAVIGATION
# ===============================================
extend type Query {
navigationById(
id: UUID!
): [NavigationItem]
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
extend type Mutation {
updateNavigation(
pageId: UUID!
mode: NavigationMode!
items: [NavigationItemInput!]
): NavigationUpdateResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type NavigationItem {
id: UUID
type: NavigationItemType
label: String
icon: String
target: String
openInNewWindow: Boolean
visibilityGroups: [UUID]
children: [NavigationItem]
}
input NavigationItemInput {
id: UUID!
type: NavigationItemType!
label: String
icon: String
target: String
openInNewWindow: Boolean
visibilityGroups: [UUID!]!
children: [NavigationItemInput!]
}
enum NavigationItemType {
header
link
separator
}
enum NavigationMode {
inherit
override
overrideExact
hide
hideExact
}
type NavigationUpdateResponse {
operation: Operation
navigationMode: NavigationMode
navigationId: UUID
}

@ -1,473 +0,0 @@
# ===============================================
# PAGES
# ===============================================
extend type Query {
pageHistoryById(
id: Int!
offsetPage: Int
offsetSize: Int
): PageHistoryResult
pageVersionById(
pageId: Int!
versionId: Int!
): PageVersion
searchPages(
"""
Site ID to search in (required)
"""
siteId: UUID!
"""
Search Query (required)
"""
query: String!
"""
The locale to perform the query as. Affects how the query is parsed by the search engine.
"""
queryLocale: String
"""
Only match pages that starts with the provided path.
"""
path: String
"""
Only match pages having one of the provided locales.
"""
locale: [String]
"""
Only match pages having one of the provided tags.
"""
tags: [String]
"""
Only match pages using the provided editor.
"""
editor: String
"""
Only match pages is the provided state.
"""
publishState: PagePublishState
"""
Result ordering. Defaults to relevancy.
"""
orderBy: PageSearchSort
"""
Result ordering direction. Defaults to descending.
"""
orderByDirection: OrderByDirection
"""
Result offset. Defaults to 0.
"""
offset: Int
"""
Results amount to return. Defaults to 25. Maximum 100.
"""
limit: Int
): PageSearchResponse
searchPagesAutocomplete(
siteId: UUID!
query: String!
): [String]
pages(
limit: Int
orderBy: PageOrderBy
orderByDirection: PageOrderByDirection
tags: [String!]
locale: String
creatorId: Int
authorId: Int
): [PageListItem]
pageById(
id: UUID!
password: String
): Page
pageByPath(
siteId: UUID!
path: String!
password: String
): Page
pageTree(
path: String
parent: Int
mode: PageTreeMode!
locale: String!
includeAncestors: Boolean
): [PageTreeItem]
pageLinks(
locale: String!
): [PageLinkItem]
pathFromAlias(
siteId: UUID!
alias: String!
): PageAliasPath
tags(
siteId: UUID!
): [PageTag]
checkConflicts(
id: Int!
checkoutDate: Date!
): Boolean
checkConflictsLatest(
id: Int!
): PageConflictLatest
}
extend type Mutation {
createPage(
alias: String
allowComments: Boolean
allowContributions: Boolean
allowRatings: Boolean
content: String!
description: String!
editor: String!
icon: String
isBrowsable: Boolean
isSearchable: Boolean
locale: String!
path: String!
publishState: PagePublishState!
publishEndDate: Date
publishStartDate: Date
relations: [PageRelationInput!]
scriptCss: String
scriptJsLoad: String
scriptJsUnload: String
showSidebar: Boolean
showTags: Boolean
showToc: Boolean
siteId: UUID!
tags: [String!]
title: String!
tocDepth: PageTocDepthInput
): PageResponse
updatePage(
id: UUID!
patch: PageUpdateInput!
): PageResponse
convertPage(
id: UUID!
editor: String!
): DefaultResponse
movePage(
id: UUID!
destinationLocale: String!
destinationPath: String!
title: String
): DefaultResponse
deletePage(
id: UUID!
): DefaultResponse
deleteTag(
id: Int!
): DefaultResponse
updateTag(
id: Int!
tag: String!
title: String!
): DefaultResponse
flushCache: DefaultResponse
migrateToLocale(
sourceLocale: String!
targetLocale: String!
): PageMigrationResponse
rebuildPageTree: DefaultResponse
rerenderPage(
id: UUID!
): DefaultResponse
restorePage(
pageId: Int!
versionId: Int!
): DefaultResponse
purgePagesHistory (
olderThan: String!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type PageResponse {
operation: Operation
page: Page
}
type PageMigrationResponse {
operation: Operation
count: Int
}
type Page {
alias: String
allowComments: Boolean
allowContributions: Boolean
allowRatings: Boolean
author: User
content: String
contentType: String
createdAt: Date
creator: User
description: String
editor: String
hash: String
icon: String
id: UUID
isBrowsable: Boolean
isSearchable: Boolean
locale: String
navigationId: UUID
navigationMode: NavigationMode
password: String
path: String
publishEndDate: Date
publishStartDate: Date
publishState: PagePublishState
relations: [PageRelation]
render: String
scriptJsLoad: String
scriptJsUnload: String
scriptCss: String
showSidebar: Boolean
showTags: Boolean
showToc: Boolean
siteId: UUID
tags: [String]
title: String
toc: [JSON]
tocDepth: PageTocDepth
updatedAt: Date
}
type PageTag {
id: UUID
tag: String
usageCount: Int
siteId: UUID
createdAt: Date
updatedAt: Date
}
type PageHistory {
versionId: Int
versionDate: Date
authorId: Int
authorName: String
actionType: String
valueBefore: String
valueAfter: String
}
type PageVersion {
action: String
authorId: String
authorName: String
content: String
contentType: String
createdAt: Date
versionDate: Date
description: String
editor: String
isPrivate: Boolean
isPublished: Boolean
locale: String
pageId: Int
path: String
publishEndDate: Date
publishStartDate: Date
render: String
tags: [String]
title: String
versionId: Int
}
type PageHistoryResult {
trail: [PageHistory]
total: Int
}
type PageSearchResponse {
results: [PageSearchResult]
totalHits: Int
}
type PageSearchResult {
description: String
highlight: String
icon: String
id: UUID
locale: String
path: String
relevancy: Float
tags: [String]
title: String
updatedAt: Date
}
type PageListItem {
id: Int
path: String
locale: String
title: String
description: String
contentType: String
isPublished: Boolean
isPrivate: Boolean
privateNS: String
createdAt: Date
updatedAt: Date
tags: [String]
}
type PageTreeItem {
id: Int
path: String
depth: Int
title: String
isPrivate: Boolean
isFolder: Boolean
privateNS: String
parent: Int
pageId: Int
locale: String
}
type PageLinkItem {
id: Int
path: String
title: String
links: [String]
}
type PageConflictLatest {
id: Int
authorId: String
authorName: String
content: String
createdAt: Date
description: String
isPublished: Boolean
locale: String
path: String
tags: [String]
title: String
updatedAt: Date
}
type PageRelation {
id: UUID
position: PageRelationPosition
label: String
caption: String
icon: String
target: String
}
input PageRelationInput {
id: UUID!
position: PageRelationPosition!
label: String!
caption: String
icon: String
target: String!
}
input PageUpdateInput {
alias: String
allowComments: Boolean
allowContributions: Boolean
allowRatings: Boolean
content: String
description: String
icon: String
isBrowsable: Boolean
isSearchable: Boolean
password: String
publishEndDate: Date
publishStartDate: Date
publishState: PagePublishState
reasonForChange: String
relations: [PageRelationInput!]
scriptJsLoad: String
scriptJsUnload: String
scriptCss: String
showSidebar: Boolean
showTags: Boolean
showToc: Boolean
tags: [String!]
title: String
tocDepth: PageTocDepthInput
}
type PageAliasPath {
id: UUID
path: String
}
type PageTocDepth {
min: Int
max: Int
}
input PageTocDepthInput {
min: Int!
max: Int!
}
enum PageSearchSort {
relevancy
title
updatedAt
}
enum PageOrderBy {
CREATED
ID
PATH
TITLE
UPDATED
}
enum PageOrderByDirection {
ASC
DESC
}
enum PageTreeMode {
FOLDERS
PAGES
ALL
}
enum PagePublishState {
draft
published
scheduled
}
enum PageRelationPosition {
left
center
right
}

@ -1,38 +0,0 @@
# ===============================================
# RENDERING
# ===============================================
extend type Query {
renderers(
filter: String
orderBy: String
): [Renderer]
}
extend type Mutation {
updateRenderers(
renderers: [RendererInput]!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type Renderer {
isEnabled: Boolean
key: String
title: String
description: String
icon: String
dependsOn: String
input: String
output: String
config: [KeyValuePair]
}
input RendererInput {
isEnabled: Boolean!
key: String!
config: [KeyValuePairInput]
}

@ -1,6 +0,0 @@
# SCALARS
scalar Date
scalar JSON
scalar Upload
scalar UUID

@ -1,246 +0,0 @@
# ===============================================
# SITES
# ===============================================
extend type Query {
sites: [Site]
siteById (
id: UUID!
): Site
siteByHostname (
hostname: String!
exact: Boolean!
): Site
}
extend type Mutation {
createSite (
hostname: String!
title: String!
): SiteCreateResponse
updateSite (
id: UUID!
patch: SiteUpdateInput!
): DefaultResponse
uploadSiteLogo (
id: UUID!
image: Upload!
): DefaultResponse
uploadSiteFavicon (
id: UUID!
image: Upload!
): DefaultResponse
uploadSiteLoginBg (
id: UUID!
image: Upload!
): DefaultResponse
deleteSite (
id: UUID!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type Site {
id: UUID
hostname: String
isEnabled: Boolean
title: String
description: String
company: String
contentLicense: String
footerExtra: String
pageExtensions: String
pageCasing: Boolean
logoText: Boolean
sitemap: Boolean
robots: SiteRobots
features: SiteFeatures
discoverable: Boolean
defaults: SiteDefaults
uploads: SiteUploads
locales: SiteLocales
editors: SiteEditors
theme: SiteTheme
}
type SiteRobots {
index: Boolean
follow: Boolean
}
type SiteFeatures {
browse: Boolean
ratings: Boolean
ratingsMode: SitePageRatingMode
comments: Boolean
contributions: Boolean
profile: Boolean
reasonForChange: SiteReasonForChangeMode
search: Boolean
}
type SiteDefaults {
tocDepth: PageTocDepth
}
type SiteUploads {
conflictBehavior: SiteUploadConflictBehavior
normalizeFilename: Boolean
}
type SiteLocales {
primary: String
active: [LocalizationLocale]
}
type SiteEditors {
asciidoc: SiteEditor
markdown: SiteEditor
wysiwyg: SiteEditor
}
type SiteEditor {
isActive: Boolean
config: JSON
}
type SiteTheme {
dark: Boolean
codeBlocksTheme: String
colorPrimary: String
colorSecondary: String
colorAccent: String
colorHeader: String
colorSidebar: String
injectCSS: String
injectHead: String
injectBody: String
contentWidth: SiteThemeWidth
sidebarPosition: SiteThemePosition
tocPosition: SiteThemePosition
showSharingMenu: Boolean
showPrintBtn: Boolean
baseFont: String
contentFont: String
}
enum SiteThemeWidth {
full
centered
}
enum SiteThemePosition {
off
left
right
}
enum SitePageRatingMode {
off
thumbs
stars
}
enum SiteReasonForChangeMode {
off
optional
required
}
enum SiteUploadConflictBehavior {
overwrite
reject
new
}
type SiteCreateResponse {
operation: Operation
site: Site
}
input SiteUpdateInput {
hostname: String
isEnabled: Boolean
title: String
description: String
company: String
contentLicense: String
footerExtra: String
pageExtensions: String
pageCasing: Boolean
logoText: Boolean
sitemap: Boolean
robots: SiteRobotsInput
features: SiteFeaturesInput
discoverable: Boolean
defaults: SiteDefaultsInput
uploads: SiteUploadsInput
editors: SiteEditorsInput
theme: SiteThemeInput
}
input SiteRobotsInput {
index: Boolean
follow: Boolean
}
input SiteFeaturesInput {
browse: Boolean
ratings: Boolean
ratingsMode: SitePageRatingMode
comments: Boolean
contributions: Boolean
profile: Boolean
reasonForChange: SiteReasonForChangeMode
search: Boolean
}
input SiteDefaultsInput {
tocDepth: PageTocDepthInput
}
input SiteThemeInput {
dark: Boolean
codeBlocksTheme: String
colorPrimary: String
colorSecondary: String
colorAccent: String
colorHeader: String
colorSidebar: String
injectCSS: String
injectHead: String
injectBody: String
contentWidth: SiteThemeWidth
sidebarPosition: SiteThemePosition
tocPosition: SiteThemePosition
showSharingMenu: Boolean
showPrintBtn: Boolean
baseFont: String
contentFont: String
}
input SiteEditorsInput {
asciidoc: SiteEditorInput
markdown: SiteEditorInput
wysiwyg: SiteEditorInput
}
input SiteEditorInput {
isActive: Boolean
config: JSON
}
input SiteUploadsInput {
conflictBehavior: SiteUploadConflictBehavior
normalizeFilename: Boolean
}

@ -1,79 +0,0 @@
# ===============================================
# STORAGE
# ===============================================
extend type Query {
storageTargets(
siteId: UUID!
): [StorageTarget]
}
extend type Mutation {
updateStorageTargets(
siteId: UUID!
targets: [StorageTargetInput]!
): DefaultResponse
setupStorageTarget(
targetId: UUID!
state: JSON!
): StorageTargetSetupResponse
destroyStorageTargetSetup(
targetId: UUID!
): DefaultResponse
executeStorageAction(
targetId: UUID!
handler: String!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type StorageTarget {
id: UUID
isEnabled: Boolean
module: String
title: String
description: String
icon: String
banner: String
vendor: String
website: String
contentTypes: JSON
assetDelivery: JSON
versioning: JSON
sync: JSON
status: JSON
setup: JSON
config: JSON
actions: JSON
}
type StorageTargetSetupResponse {
operation: Operation
state: JSON
}
input StorageTargetInput {
id: UUID!
module: String!
isEnabled: Boolean
contentTypes: [String!]
largeThreshold: String
assetDeliveryFileStreaming: Boolean
assetDeliveryDirectAccess: Boolean
syncMode: StorageTargetSyncMode
syncInterval: String
useVersioning: Boolean
config: JSON
}
enum StorageTargetSyncMode {
PULL
PUSH
SYNC
}

@ -1,234 +0,0 @@
# ===============================================
# SYSTEM
# ===============================================
extend type Query {
metricsState: Boolean
systemExtensions: [SystemExtension]
systemFlags: JSON
systemInfo: SystemInfo
systemInstances: [SystemInstance]
systemSecurity: SystemSecurity
systemJobs(
states: [SystemJobState]
): [SystemJob]
systemJobsScheduled: [SystemJobScheduled]
systemJobsUpcoming: [SystemJobUpcoming]
systemSearch: SystemSearch
}
extend type Mutation {
cancelJob(
id: UUID!
): DefaultResponse
checkForUpdates: SystemCheckUpdateResponse
disconnectWS: DefaultResponse
installExtension(
key: String!
): DefaultResponse
rebuildSearchIndex: DefaultResponse
retryJob(
id: UUID!
): DefaultResponse
setMetricsState(
enabled: Boolean!
): DefaultResponse
updateSystemSearch(
termHighlighting: Boolean
dictOverrides: String
): DefaultResponse
updateSystemFlags(
flags: JSON!
): DefaultResponse
updateSystemSecurity(
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
corsConfig: String
corsMode: SystemSecurityCorsMode
cspDirectives: String
disallowFloc: Boolean
disallowIframe: Boolean
disallowOpenRedirect: Boolean
enforceCsp: Boolean
enforceHsts: Boolean
enforceSameOriginReferrerPolicy: Boolean
forceAssetDownload: Boolean
hstsDuration: Int
trustProxy: Boolean
uploadMaxFiles: Int
uploadMaxFileSize: Int
uploadScanSVG: Boolean
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type SystemInfo {
configFile: String
cpuCores: Int
currentVersion: String
dbHost: String
dbType: String
dbVersion: String
groupsTotal: Int
hostname: String
httpPort: Int
httpRedirection: Boolean
httpsPort: Int
isMailConfigured: Boolean
isSchedulerHealthy: Boolean
latestVersion: String
latestVersionReleaseDate: Date
loginsPastDay: Int
nodeVersion: String
operatingSystem: String
pagesTotal: Int
platform: String
ramTotal: String
sslDomain: String
sslExpirationDate: Date
sslProvider: String
sslStatus: String
sslSubscriberEmail: String
tagsTotal: Int
upgradeCapable: Boolean
usersTotal: Int
workingDirectory: String
}
type SystemInstance {
id: String
activeConnections: Int
activeListeners: Int
dbUser: String
dbFirstSeen: Date
dbLastSeen: Date
ip: String
}
enum SystemImportUsersGroupMode {
MULTI
SINGLE
NONE
}
type SystemImportUsersResponse {
operation: Operation
usersCount: Int
groupsCount: Int
failed: [SystemImportUsersResponseFailed]
}
type SystemImportUsersResponseFailed {
provider: String
email: String
error: String
}
type SystemExtension {
key: String
title: String
description: String
isInstalled: Boolean
isInstallable: Boolean
isCompatible: Boolean
}
type SystemSecurity {
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
corsConfig: String
corsMode: SystemSecurityCorsMode
cspDirectives: String
disallowFloc: Boolean
disallowIframe: Boolean
disallowOpenRedirect: Boolean
enforceCsp: Boolean
enforceHsts: Boolean
enforceSameOriginReferrerPolicy: Boolean
forceAssetDownload: Boolean
hstsDuration: Int
trustProxy: Boolean
uploadMaxFiles: Int
uploadMaxFileSize: Int
uploadScanSVG: Boolean
}
enum SystemSecurityCorsMode {
OFF
REFLECT
HOSTNAMES
REGEX
}
type SystemJob {
id: UUID
task: String
state: SystemJobState
useWorker: Boolean
wasScheduled: Boolean
payload: JSON
attempt: Int
maxRetries: Int
lastErrorMessage: String
executedBy: String
createdAt: Date
startedAt: Date
completedAt: Date
}
type SystemJobScheduled {
id: UUID
task: String
cron: String
type: String
payload: JSON
createdAt: Date
updatedAt: Date
}
type SystemJobUpcoming {
id: UUID
task: String
useWorker: Boolean
payload: JSON
retries: Int
maxRetries: Int
waitUntil: Date
isScheduled: Boolean
createdBy: String
createdAt: Date
updatedAt: Date
}
enum SystemJobState {
ACTIVE
COMPLETED
FAILED
INTERRUPTED
}
type SystemCheckUpdateResponse {
operation: Operation
current: String
latest: String
latestDate: String
}
type SystemSearch {
termHighlighting: Boolean
dictOverrides: String
}

@ -1,132 +0,0 @@
# ===============================================
# TREE
# ===============================================
extend type Query {
"""
Browse the tree.
Must provide either `parentId` or a combination of `parentPath` and `locale`.
"""
tree(
siteId: UUID!
parentId: UUID
parentPath: String
locale: String
types: [TreeItemType]
tags: [String]
limit: Int
offset: Int
orderBy: TreeOrderBy
orderByDirection: OrderByDirection
"""
How many levels of children to include. Defaults to 1.
"""
depth: Int
"""
Include all parent folders up to root
"""
includeAncestors: Boolean
"""
Include all folders at root level
"""
includeRootFolders: Boolean
): [TreeItem]
folderById(
id: UUID!
): TreeItemFolder
folderByPath(
siteId: UUID!
locale: String!
path: String!
): TreeItemFolder
}
extend type Mutation {
createFolder(
siteId: UUID!
locale: String!
parentId: UUID
parentPath: String
pathName: String!
title: String!
): DefaultResponse
deleteFolder(
folderId: UUID!
): DefaultResponse
duplicateFolder(
folderId: UUID!
targetParentId: UUID
targetPathName: String!
targetTitle: String!
): DefaultResponse
moveFolder(
folderId: UUID!
targetParentId: UUID
): DefaultResponse
renameFolder(
folderId: UUID!
pathName: String!
title: String!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
enum TreeItemType {
asset
folder
page
}
enum TreeOrderBy {
createdAt
fileName
title
updatedAt
}
interface TreeItem {
id: UUID
folderPath: String
fileName: String
title: String
}
type TreeItemFolder implements TreeItem {
id: UUID
childrenCount: Int
depth: Int
fileName: String
folderPath: String
title: String
isAncestor: Boolean
}
type TreeItemPage implements TreeItem {
id: UUID
createdAt: Date
depth: Int
fileName: String
folderPath: String
editor: String
pageType: String
title: String
description: String
updatedAt: Date
}
type TreeItemAsset implements TreeItem {
id: UUID
createdAt: Date
depth: Int
fileName: String
# In Bytes
fileSize: Int
fileExt: String
mimeType: String
folderPath: String
title: String
updatedAt: Date
}

@ -1,221 +0,0 @@
# ===============================================
# USERS
# ===============================================
extend type Query {
users (
page: Int
pageSize: Int
orderBy: UserOrderBy
orderByDirection: OrderByDirection
# Filter by name / email
filter: String
): UserResults
userById(
id: UUID!
): User
userDefaults: UserDefaults
userEditorSettings(
editor: String!
): JSON
lastLogins: [UserLastLogin]
userPermissions: [String]
userPermissionsAtPath(
siteId: UUID!
path: String!
): [String]
}
extend type Mutation {
createUser(
email: String!
name: String!
password: String!
groups: [UUID]!
mustChangePassword: Boolean!
sendWelcomeEmail: Boolean!
sendWelcomeEmailFromSiteId: UUID
): UserResponse
updateUser(
id: UUID!
patch: UserUpdateInput!
): DefaultResponse
deleteUser(
id: UUID!
replaceId: UUID!
): DefaultResponse
verifyUser(
id: UUID!
): DefaultResponse
activateUser(
id: UUID!
): DefaultResponse
deactivateUser(
id: UUID!
): DefaultResponse
enableUserTFA(
id: UUID!
): DefaultResponse
disableUserTFA(
id: UUID!
): DefaultResponse
changeUserPassword(
id: UUID!
newPassword: String!
mustChangePassword: Boolean
): DefaultResponse
updateProfile(
name: String
location: String
jobTitle: String
pronouns: String
timezone: String
dateFormat: String
timeFormat: String
appearance: UserSiteAppearance
cvd: UserCvdChoices
): DefaultResponse
uploadUserAvatar(
id: UUID!
image: Upload!
): DefaultResponse
clearUserAvatar(
id: UUID!
): DefaultResponse
updateUserDefaults(
timezone: String!
dateFormat: String!
timeFormat: String!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type UserResponse {
operation: Operation
user: User
}
type UserLastLogin {
id: UUID
name: String
lastLoginAt: Date
}
type UserResults {
users: [UserMinimal]
total: Int
}
type UserMinimal {
id: UUID
name: String
email: String
isSystem: Boolean
isActive: Boolean
createdAt: Date
lastLoginAt: Date
}
type User {
id: UUID
name: String
email: String
auth: [UserAuth]
passkeys: [UserPasskey]
hasAvatar: Boolean
isSystem: Boolean
isActive: Boolean
isVerified: Boolean
meta: JSON
prefs: JSON
createdAt: Date
updatedAt: Date
lastLoginAt: Date
groups: [Group]
}
type UserAuth {
authId: UUID
authName: String
strategyKey: String
strategyIcon: String
config: JSON
}
type UserPasskey {
id: String
name: String
createdAt: Date
siteHostname: String
}
type UserDefaults {
timezone: String
dateFormat: String
timeFormat: String
}
type UserTokenResponse {
operation: Operation
jwt: String
}
enum UserOrderBy {
id
email
name
createdAt
updatedAt
lastLoginAt
}
enum UserSiteAppearance {
site
light
dark
}
enum UserCvdChoices {
none
protanopia
deuteranopia
tritanopia
}
input UserUpdateInput {
email: String
name: String
groups: [UUID!]
auth: UserAuthUpdateInput
isActive: Boolean
isVerified: Boolean
meta: JSON
prefs: JSON
}
input UserAuthUpdateInput {
tfaRequired: Boolean
mustChangePwd: Boolean
restrictLogin: Boolean
}

@ -1,150 +0,0 @@
import AbstractClientStore from 'express-brute/lib/AbstractClientStore.js'
const KnexStore = function (options) {
options = options || Object.create(null)
AbstractClientStore.apply(this, arguments)
this.options = Object.assign(Object.create(null), KnexStore.defaults, options)
if (this.options.knex) {
this.knex = this.options.knex
} else {
this.knex = require('knex')(KnexStore.defaultsKnex)
}
if (options.createTable === false) {
this.ready = Promise.resolve()
} else {
this.ready = this.knex.schema.hasTable(this.options.tablename)
.then((exists) => {
if (exists) {
return
}
return this.knex.schema.createTable(this.options.tablename, (table) => {
table.string('key')
table.bigInteger('firstRequest').nullable()
table.bigInteger('lastRequest').nullable()
table.bigInteger('lifetime').nullable()
table.integer('count')
})
})
}
}
KnexStore.prototype = Object.create(AbstractClientStore.prototype)
KnexStore.prototype.set = async function (key, value, lifetime, callback) {
try {
lifetime = lifetime || 0
await this.ready
const resp = await this.knex.transaction((trx) => {
return trx
.select('*')
.forUpdate()
.from(this.options.tablename)
.where('key', '=', key)
.then((foundKeys) => {
if (foundKeys.length === 0) {
return trx.from(this.options.tablename)
.insert({
key: key,
lifetime: new Date(Date.now() + lifetime * 1000).getTime(),
lastRequest: new Date(value.lastRequest).getTime(),
firstRequest: new Date(value.firstRequest).getTime(),
count: value.count
})
} else {
return trx(this.options.tablename)
.where('key', '=', key)
.update({
lifetime: new Date(Date.now() + lifetime * 1000).getTime(),
count: value.count,
lastRequest: new Date(value.lastRequest).getTime()
})
}
})
})
callback(null, resp)
} catch (err) {
callback(err, null)
}
}
KnexStore.prototype.get = async function (key, callback) {
try {
await this.ready
await this.clearExpired()
const resp = await this.knex.select('*')
.from(this.options.tablename)
.where('key', '=', key)
let o = null
if (resp[0]) {
o = {}
o.lastRequest = new Date(resp[0].lastRequest)
o.firstRequest = new Date(resp[0].firstRequest)
o.count = resp[0].count
}
callback(null, o)
} catch (err) {
callback(err, null)
}
}
KnexStore.prototype.reset = async function (key, callback) {
try {
await this.ready
const resp = await this.knex(this.options.tablename)
.where('key', '=', key)
.del()
callback(null, resp)
} catch (err) {
callback(err, null)
}
}
KnexStore.prototype.increment = async function (key, lifetime, callback) {
try {
const result = await this.get(key)
let resp = null
if (result) {
resp = await this.knex(this.options.tablename)
.increment('count', 1)
.where('key', '=', key)
} else {
resp = await this.knex(this.options.tablename)
.insert({
key: key,
firstRequest: new Date().getTime(),
lastRequest: new Date().getTime(),
lifetime: new Date(Date.now() + lifetime * 1000).getTime(),
count: 1
})
}
callback(null, resp)
} catch (err) {
callback(err, null)
}
}
KnexStore.prototype.clearExpired = async function (callback) {
await this.ready
return this.knex(this.options.tablename)
.del()
.where('lifetime', '<', new Date().getTime())
}
KnexStore.defaults = {
tablename: 'brute',
createTable: true
}
KnexStore.defaultsKnex = {
client: 'sqlite3',
// debug: true,
connection: {
filename: './brute-knex.sqlite'
}
}
export default KnexStore

@ -1,112 +0,0 @@
import { isNil, isPlainObject, set, startCase, transform } from 'lodash-es'
import crypto from 'node:crypto'
/* eslint-disable promise/param-names */
export function createDeferred () {
let result, resolve, reject
return {
resolve: function (value) {
if (resolve) {
resolve(value)
} else {
result = result || new Promise(function (r) { r(value) })
}
},
reject: function (reason) {
if (reject) {
reject(reason)
} else {
result = result || new Promise(function (x, j) { j(reason) })
}
},
promise: new Promise(function (r, j) {
if (result) {
r(result)
} else {
resolve = r
reject = j
}
})
}
}
/**
* Decode a tree path
*
* @param {string} str String to decode
* @returns Decoded tree path
*/
export function decodeTreePath (str) {
return str?.replaceAll('.', '/')
}
/**
* Encode a tree path
*
* @param {string} str String to encode
* @returns Encoded tree path
*/
export function encodeTreePath (str) {
return str?.toLowerCase()?.replaceAll('/', '.') || ''
}
/**
* Generate SHA-1 Hash of a string
*
* @param {string} str String to hash
* @returns Hashed string
*/
export function generateHash (str) {
return crypto.createHash('sha1').update(str).digest('hex')
}
/**
* Get default value of type
*
* @param {any} type primitive type name
* @returns Default value
*/
export function getTypeDefaultValue (type) {
switch (type.toLowerCase()) {
case 'string':
return ''
case 'number':
return 0
case 'boolean':
return false
}
}
export function parseModuleProps (props) {
return transform(props, (result, value, key) => {
let defaultValue = ''
if (isPlainObject(value)) {
defaultValue = !isNil(value.default) ? value.default : getTypeDefaultValue(value.type)
} else {
defaultValue = getTypeDefaultValue(value)
}
set(result, key, {
default: defaultValue,
type: (value.type || value).toLowerCase(),
title: value.title || startCase(key),
hint: value.hint || '',
enum: value.enum || false,
enumDisplay: value.enumDisplay || 'select',
multiline: value.multiline || false,
sensitive: value.sensitive || false,
icon: value.icon || 'rename',
order: value.order || 100,
if: value.if ?? []
})
return result
}, {})
}
export function getDictNameFromLocale (locale) {
const loc = locale.length > 2 ? locale.substring(0, 2) : locale
if (loc in WIKI.config.search.dictOverrides) {
return WIKI.config.search.dictOverrides[loc]
} else {
return WIKI.data.tsDictMappings[loc] ?? 'simple'
}
}

@ -1,27 +0,0 @@
import { replace } from 'lodash-es'
const isoDurationReg = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/
export default {
/**
* Parse configuration value for environment vars
*
* Replaces `$(ENV_VAR_NAME)` with value of `ENV_VAR_NAME` environment variable.
*
* Also supports defaults by if provided as `$(ENV_VAR_NAME:default)`
*
* @param {any} cfg Configuration value
* @returns Parse configuration value
*/
parseConfigValue (cfg) {
return replace(
cfg,
/\$\(([A-Z0-9_]+)(?::(.+))?\)/g,
(fm, m, d) => { return process.env[m] || d }
)
},
isValidDurationString (val) {
return isoDurationReg.test(val)
}
}

@ -1,187 +0,0 @@
import CustomError from 'custom-error-instance'
export default {
Custom (slug, message) {
return CustomError(slug, { message })
},
AssetDeleteForbidden: CustomError('AssetDeleteForbidden', {
message: 'You are not authorized to delete this asset.'
}),
AssetFolderExists: CustomError('AssetFolderExists', {
message: 'An asset folder with the same name already exists.'
}),
AssetGenericError: CustomError('AssetGenericError', {
message: 'An unexpected error occured during asset operation.'
}),
AssetInvalid: CustomError('AssetInvalid', {
message: 'This asset does not exist or is invalid.'
}),
AssetRenameCollision: CustomError('AssetRenameCollision', {
message: 'An asset with the same filename in the same folder already exists.'
}),
AssetRenameForbidden: CustomError('AssetRenameForbidden', {
message: 'You are not authorized to rename this asset.'
}),
AssetRenameInvalid: CustomError('AssetRenameInvalid', {
message: 'The new asset filename is invalid.'
}),
AssetRenameInvalidExt: CustomError('AssetRenameInvalidExt', {
message: 'The file extension cannot be changed on an existing asset.'
}),
AssetRenameTargetForbidden: CustomError('AssetRenameTargetForbidden', {
message: 'You are not authorized to rename this asset to the requested name.'
}),
AuthAccountBanned: CustomError('AuthAccountBanned', {
message: 'Your account has been disabled.'
}),
AuthAccountAlreadyExists: CustomError('AuthAccountAlreadyExists', {
message: 'An account already exists using this email address.'
}),
AuthAccountNotVerified: CustomError('AuthAccountNotVerified', {
message: 'You must verify your account before your can login.'
}),
AuthGenericError: CustomError('AuthGenericError', {
message: 'An unexpected error occured during login.'
}),
AuthLoginFailed: CustomError('AuthLoginFailed', {
message: 'Invalid email / username or password.'
}),
AuthPasswordInvalid: CustomError('AuthPasswordInvalid', {
message: 'Password is incorrect.'
}),
AuthProviderInvalid: CustomError('AuthProviderInvalid', {
message: 'Invalid authentication provider.'
}),
AuthRegistrationDisabled: CustomError('AuthRegistrationDisabled', {
message: 'Registration is disabled. Contact your system administrator.'
}),
AuthRegistrationDomainUnauthorized: CustomError('AuthRegistrationDomainUnauthorized', {
message: 'You are not authorized to register. Your domain is not whitelisted.'
}),
AuthRequired: CustomError('AuthRequired', {
message: 'You must be authenticated to access this resource.'
}),
AuthTFAFailed: CustomError('AuthTFAFailed', {
message: 'Incorrect TFA Security Code.'
}),
AuthTFAInvalid: CustomError('AuthTFAInvalid', {
message: 'Invalid TFA Security Code or Login Token.'
}),
AuthValidationTokenInvalid: CustomError('AuthValidationTokenInvalid', {
message: 'Invalid validation token.'
}),
BruteInstanceIsInvalid: CustomError('BruteInstanceIsInvalid', {
message: 'Invalid Brute Force Instance.'
}),
BruteTooManyAttempts: CustomError('BruteTooManyAttempts', {
message: 'Too many attempts! Try again later.'
}),
CommentContentMissing: CustomError('CommentContentMissing', {
message: 'Comment content is missing or too short.'
}),
CommentGenericError: CustomError('CommentGenericError', {
message: 'An unexpected error occured.'
}),
CommentManageForbidden: CustomError('CommentManageForbidden', {
message: 'You are not authorized to manage comments on this page.'
}),
CommentNotFound: CustomError('CommentNotFound', {
message: 'This comment does not exist.'
}),
CommentPostForbidden: CustomError('CommentPostForbidden', {
message: 'You are not authorized to post a comment on this page.'
}),
CommentViewForbidden: CustomError('CommentViewForbidden', {
message: 'You are not authorized to view comments for this page.'
}),
InputInvalid: CustomError('InputInvalid', {
message: 'Input data is invalid.'
}),
LocaleGenericError: CustomError('LocaleGenericError', {
message: 'An unexpected error occured during locale operation.'
}),
LocaleInvalidNamespace: CustomError('LocaleInvalidNamespace', {
message: 'Invalid locale or namespace.'
}),
MailGenericError: CustomError('MailGenericError', {
message: 'An unexpected error occured during mail operation.'
}),
MailInvalidRecipient: CustomError('MailInvalidRecipient', {
message: 'The recipient email address is invalid.'
}),
MailNotConfigured: CustomError('MailNotConfigured', {
message: 'The mail configuration is incomplete or invalid.'
}),
MailTemplateFailed: CustomError('MailTemplateFailed', {
message: 'Mail template failed to load.'
}),
PageCreateForbidden: CustomError('PageCreateForbidden', {
message: 'You are not authorized to create this page.'
}),
PageDeleteForbidden: CustomError('PageDeleteForbidden', {
message: 'You are not authorized to delete this page.'
}),
PageGenericError: CustomError('PageGenericError', {
message: 'An unexpected error occured during a page operation.'
}),
PageDuplicateCreate: CustomError('PageDuplicateCreate', {
message: 'Cannot create this page because an entry already exists at the same path.'
}),
PageEmptyContent: CustomError('PageEmptyContent', {
message: 'Page content cannot be empty.'
}),
PageHistoryForbidden: CustomError('PageHistoryForbidden', {
message: 'You are not authorized to view the history of this page.'
}),
PageIllegalPath: CustomError('PageIllegalPath', {
message: 'Page path cannot contains illegal characters.'
}),
PageMoveForbidden: CustomError('PageMoveForbidden', {
message: 'You are not authorized to move this page.'
}),
PageNotFound: CustomError('PageNotFound', {
message: 'This page does not exist.'
}),
PagePathCollision: CustomError('PagePathCollision', {
message: 'Destination page path already exists.'
}),
PageRestoreForbidden: CustomError('PageRestoreForbidden', {
message: 'You are not authorized to restore this page version.'
}),
PageUpdateForbidden: CustomError('PageUpdateForbidden', {
message: 'You are not authorized to update this page.'
}),
PageViewForbidden: CustomError('PageViewForbidden', {
message: 'You are not authorized to view this page.'
}),
SearchActivationFailed: CustomError('SearchActivationFailed', {
message: 'Search Engine activation failed.'
}),
SearchGenericError: CustomError('SearchGenericError', {
message: 'An unexpected error occured during search operation.'
}),
SystemGenericError: CustomError('SystemGenericError', {
message: 'An unexpected error occured.'
}),
SystemSSLDisabled: CustomError('SystemSSLDisabled', {
message: 'SSL is not enabled.'
}),
SystemSSLLEUnavailable: CustomError('SystemSSLLEUnavailable', {
message: 'Let\'s Encrypt is not initialized.'
}),
SystemSSLRenewInvalidProvider: CustomError('SystemSSLRenewInvalidProvider', {
message: 'Current provider does not support SSL certificate renewal.'
}),
UserCreationFailed: CustomError('UserCreationFailed', {
message: 'An unexpected error occured during user creation.'
}),
UserDeleteForeignConstraint: CustomError('UserDeleteForeignConstraint', {
message: 'Cannot delete user because of content relational constraints.'
}),
UserDeleteProtected: CustomError('UserDeleteProtected', {
message: 'Cannot delete a protected system account.'
}),
UserNotFound: CustomError('UserNotFound', {
message: 'This user does not exist.'
})
}

@ -1,20 +0,0 @@
import { defaultTo, isFinite } from 'lodash-es'
export function generateSuccess (msg) {
return {
succeeded: true,
errorCode: 0,
slug: 'ok',
message: defaultTo(msg, 'Operation succeeded.')
}
}
export function generateError (err, complete = true) {
const error = {
succeeded: false,
errorCode: isFinite(err.code) ? err.code : 1,
slug: err.name,
message: err.message || 'An unexpected error occured.'
}
return (complete) ? { operation: error } : error
}

@ -1,153 +0,0 @@
import qs from 'querystring'
import { fromPairs, get, head, initial, invert, isEmpty, last } from 'lodash-es'
import crypto from 'node:crypto'
import path from 'node:path'
const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i
// eslint-disable-next-line no-control-regex
const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/
const contentToExt = {
markdown: 'md',
html: 'html'
}
const extToContent = invert(contentToExt)
/**
* Parse raw url path and make it safe
*/
export function parsePath (rawPath, opts = {}) {
const pathObj = {
// TODO: use site base lang
locale: 'en', // WIKI.config.lang.code,
path: 'home',
explicitLocale: false
}
// Clean Path
rawPath = qs.unescape(rawPath).trim()
if (rawPath.startsWith('/')) { rawPath = rawPath.substring(1) }
rawPath = rawPath.replace(unsafeCharsRegex, '')
if (rawPath === '') { rawPath = 'home' }
rawPath = rawPath.replace(/\\/g, '').replace(/\/\//g, '').replace(/\.\.+/ig, '')
// Extract Info
let pathParts = rawPath.split('/').filter(p => {
p = p.trim()
return !isEmpty(p) && p !== '..' && p !== '.'
})
if (pathParts.length > 1 && pathParts[0].startsWith('_')) {
pathParts.shift()
}
if (localeSegmentRegex.test(pathParts[0])) {
pathObj.locale = pathParts[0]
pathObj.explicitLocale = true
pathParts.shift()
}
// Strip extension
if (opts.stripExt && pathParts.length > 0) {
const lastPart = last(pathParts)
if (lastPart.indexOf('.') > 0) {
pathParts.pop()
const lastPartMeta = path.parse(lastPart)
pathParts.push(lastPartMeta.name)
}
}
pathObj.path = pathParts.join('/')
return pathObj
}
/**
* Generate unique hash from page
*/
export function generateHash(opts) {
return crypto.createHash('sha1').update(`${opts.locale}|${opts.path}|${opts.privateNS}`).digest('hex')
}
/**
* Inject Page Metadata
*/
export function injectPageMetadata(page) {
const meta = [
['title', page.title],
['description', page.description],
['published', page.isPublished.toString()],
['date', page.updatedAt],
['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''],
['editor', page.editorKey],
['dateCreated', page.createdAt]
]
switch (page.contentType) {
case 'markdown':
return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content
case 'html':
return '<!--\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n-->\n\n' + page.content
case 'json':
return {
...page.content,
_meta: fromPairs(meta)
}
default:
return page.content
}
}
/**
* Check if path is a reserved path
*/
export function isReservedPath(rawPath) {
const firstSection = head(rawPath.split('/'))
if (firstSection.length < 1) {
return true
} else if (localeSegmentRegex.test(firstSection)) {
return true
} else if (
WIKI.data.reservedPaths.some(p => {
return p === firstSection
})) {
return true
} else {
return false
}
}
/**
* Get file extension from content type
*/
export function getFileExtension(contentType) {
return get(contentToExt, contentType, 'txt')
}
/**
* Get content type from file extension
*/
export function getContentType (filePath) {
const ext = last(filePath.split('.'))
return get(extToContent, ext, false)
}
/**
* Get Page Meta object from disk path
*/
export function getPagePath (filePath) {
let fpath = filePath
if (process.platform === 'win32') {
fpath = filePath.replace(/\\/g, '/')
}
let meta = {
locale: WIKI.config.lang.code,
path: initial(fpath.split('.')).join('')
}
const result = localeFolderRegex.exec(meta.path)
if (result[1]) {
meta = {
locale: result[1].replace('/', ''),
path: result[2]
}
}
return meta
}

@ -1,38 +0,0 @@
import util from 'node:util'
import crypto from 'node:crypto'
import passportJWT from 'passport-jwt'
const randomBytes = util.promisify(crypto.randomBytes)
export function sanitizeCommitUser (user) {
// let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')
// return {
// name: _.chain(user.name).replace(wlist, '').trim().value(),
// email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail
// }
}
/**
* Generate a random token
*
* @param {any} length
* @returns
*/
export async function generateToken (length) {
return (await randomBytes(length)).toString('hex')
}
export const extractJWT = passportJWT.ExtractJwt.fromExtractors([
passportJWT.ExtractJwt.fromAuthHeaderAsBearerToken(),
(req) => {
let token = null
if (req && req.cookies) {
token = req.cookies['jwt']
}
// Force uploads to use Auth headers
if (req.path.toLowerCase() === '/u') {
return null
}
return token
}
])

@ -1,75 +0,0 @@
// ===========================================
// Wiki.js Server
// Licensed under AGPLv3
// ===========================================
import path from 'node:path'
import { DateTime } from 'luxon'
import semver from 'semver'
import { customAlphabet } from 'nanoid'
import fse from 'fs-extra'
import configSvc from './core/config.mjs'
import kernel from './core/kernel.mjs'
import logger from './core/logger.mjs'
const nanoid = customAlphabet('1234567890abcdef', 10)
if (!semver.satisfies(process.version, '>=24')) {
console.error('ERROR: Node.js 24.x or later required!')
process.exit(1)
}
if (fse.pathExistsSync('./package.json')) {
console.error('ERROR: Must run server from the parent directory!')
process.exit(1)
}
const WIKI = {
IS_DEBUG: process.env.NODE_ENV === 'development',
ROOTPATH: process.cwd(),
INSTANCE_ID: nanoid(10),
SERVERPATH: path.join(process.cwd(), 'server'),
configSvc,
kernel,
sites: {},
sitesMappings: {},
startedAt: DateTime.utc(),
storage: {
defs: [],
modules: []
}
}
global.WIKI = WIKI
if (WIKI.IS_DEBUG) {
process.on('warning', (warning) => {
console.log(warning.stack)
})
}
await WIKI.configSvc.init()
// ----------------------------------------
// Init Logger
// ----------------------------------------
WIKI.logger = logger.init()
// ----------------------------------------
// Start Kernel
// ----------------------------------------
WIKI.kernel.init()
// ----------------------------------------
// Register exit handler
// ----------------------------------------
process.on('SIGINT', () => {
WIKI.kernel.shutdown()
})
process.on('message', (msg) => {
if (msg === 'shutdown') {
WIKI.kernel.shutdown()
}
})

File diff suppressed because it is too large Load Diff

@ -1,81 +0,0 @@
const localazyMetadata = {
projectUrl: "https://localazy.com/p/wiki",
baseLocale: "en",
languages: [
{
language: "de",
region: "",
script: "",
isRtl: false,
name: "German",
localizedName: "Deutsch",
pluralType: (n) => { return (n===1) ? "one" : "other"; }
},
{
language: "en",
region: "",
script: "",
isRtl: false,
name: "English",
localizedName: "English",
pluralType: (n) => { return (n===1) ? "one" : "other"; }
},
{
language: "fr",
region: "",
script: "",
isRtl: false,
name: "French",
localizedName: "Français",
pluralType: (n) => { return (n===0 || n===1) ? "one" : "other"; }
},
{
language: "pt",
region: "BR",
script: "",
isRtl: false,
name: "Brazilian Portuguese",
localizedName: "Português (Brasil)",
pluralType: (n) => { return (n>=0 && n<=1) ? "one" : "other"; }
},
{
language: "ru",
region: "",
script: "",
isRtl: false,
name: "Russian",
localizedName: "Русский",
pluralType: (n) => { return ((n%10===1) && (n%100!==11)) ? "one" : ((n%10>=2 && n%10<=4) && ((n%100<12 || n%100>14))) ? "few" : "many"; }
},
{
language: "zh",
region: "",
script: "Hans",
isRtl: false,
name: "Simplified Chinese",
localizedName: "简体中文",
pluralType: (n) => { return "other"; }
}
],
files: [
{
cdnHash: "54b977214afbffe2ffeb07d0ccb03558e75e4408",
file: "file.json",
path: "",
library: "",
module: "",
buildType: "",
productFlavors: [],
cdnFiles: {
"de#": "https://delivery.localazy.com/_a7797965569058078203416ae5aa/_e0/54b977214afbffe2ffeb07d0ccb03558e75e4408/de/file.json",
"en#": "https://delivery.localazy.com/_a7797965569058078203416ae5aa/_e0/54b977214afbffe2ffeb07d0ccb03558e75e4408/en/file.json",
"fr#": "https://delivery.localazy.com/_a7797965569058078203416ae5aa/_e0/54b977214afbffe2ffeb07d0ccb03558e75e4408/fr/file.json",
"pt_BR#": "https://delivery.localazy.com/_a7797965569058078203416ae5aa/_e0/54b977214afbffe2ffeb07d0ccb03558e75e4408/pt-BR/file.json",
"ru#": "https://delivery.localazy.com/_a7797965569058078203416ae5aa/_e0/54b977214afbffe2ffeb07d0ccb03558e75e4408/ru/file.json",
"zh#Hans": "https://delivery.localazy.com/_a7797965569058078203416ae5aa/_e0/54b977214afbffe2ffeb07d0ccb03558e75e4408/zh-Hans/file.json"
}
}
]
};
export default localazyMetadata;

@ -1,102 +0,0 @@
import { Model } from 'objection'
import fs from 'node:fs/promises'
import path from 'node:path'
import { defaultTo, forOwn, isBoolean, replace, sortBy } from 'lodash-es'
import yaml from 'js-yaml'
import { parseModuleProps } from '../helpers/common.mjs'
/**
* Analytics model
*/
export class Analytics extends Model {
static get tableName() { return 'analytics' }
static get jsonSchema () {
return {
type: 'object',
required: ['module', 'isEnabled'],
properties: {
id: { type: 'string' },
module: { type: 'string' },
isEnabled: { type: 'boolean', default: false }
}
}
}
static get jsonAttributes() {
return ['config']
}
static async getProviders(isEnabled) {
const providers = await WIKI.db.analytics.query().where(isBoolean(isEnabled) ? { isEnabled } : {})
return sortBy(providers, ['module'])
}
static async refreshProvidersFromDisk() {
try {
// -> Fetch definitions from disk
const analyticsDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/analytics'))
WIKI.data.analytics = []
for (const dir of analyticsDirs) {
const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/analytics', dir, 'definition.yml'), 'utf8')
const defParsed = yaml.load(def)
defParsed.key = dir
defParsed.props = parseModuleProps(defParsed.props)
WIKI.data.analytics.push(defParsed)
WIKI.logger.debug(`Loaded analytics module definition ${dir}: [ OK ]`)
}
WIKI.logger.info(`Loaded ${WIKI.data.analytics.length} analytics module definitions: [ OK ]`)
} catch (err) {
WIKI.logger.error(`Failed to scan or load analytics providers: [ FAILED ]`)
WIKI.logger.error(err)
}
}
static async getCode ({ cache = false } = {}) {
if (cache) {
const analyticsCached = await WIKI.cache.get('analytics')
if (analyticsCached) {
return analyticsCached
}
}
try {
const analyticsCode = {
head: '',
bodyStart: '',
bodyEnd: ''
}
const providers = await WIKI.db.analytics.getProviders(true)
for (let provider of providers) {
const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/analytics', provider.key, 'code.yml'), 'utf8')
let code = yaml.safeLoad(def)
code.head = defaultTo(code.head, '')
code.bodyStart = defaultTo(code.bodyStart, '')
code.bodyEnd = defaultTo(code.bodyEnd, '')
forOwn(provider.config, (value, key) => {
code.head = replace(code.head, new RegExp(`{{${key}}}`, 'g'), value)
code.bodyStart = replace(code.bodyStart, `{{${key}}}`, value)
code.bodyEnd = replace(code.bodyEnd, `{{${key}}}`, value)
})
analyticsCode.head += code.head
analyticsCode.bodyStart += code.bodyStart
analyticsCode.bodyEnd += code.bodyEnd
}
await WIKI.cache.set('analytics', analyticsCode, 300)
return analyticsCode
} catch (err) {
WIKI.logger.warn('Error while getting analytics code: ', err)
return {
head: '',
bodyStart: '',
bodyEnd: ''
}
}
}
}

@ -1,75 +0,0 @@
/* global WIKI */
import { Model } from 'objection'
import { DateTime } from 'luxon'
import ms from 'ms'
import jwt from 'jsonwebtoken'
/**
* Users model
*/
export class ApiKey extends Model {
static get tableName() { return 'apiKeys' }
static get jsonSchema () {
return {
type: 'object',
required: ['name', 'key'],
properties: {
id: {type: 'string'},
name: {type: 'string'},
key: {type: 'string'},
expiration: {type: 'string'},
isRevoked: {type: 'boolean'},
createdAt: {type: 'string'},
validUntil: {type: 'string'}
}
}
}
async $beforeUpdate(opt, context) {
await super.$beforeUpdate(opt, context)
this.updatedAt = new Date().toISOString()
}
async $beforeInsert(context) {
await super.$beforeInsert(context)
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
static async createNewKey ({ name, expiration, groups }) {
console.info(DateTime.utc().plus(ms(expiration)).toISO())
const entry = await WIKI.db.apiKeys.query().insert({
name,
key: 'pending',
expiration: DateTime.utc().plus(ms(expiration)).toISO(),
isRevoked: true
})
console.info(entry)
const key = jwt.sign({
api: entry.id,
grp: groups
}, {
key: WIKI.config.auth.certs.private,
passphrase: WIKI.config.auth.secret
}, {
algorithm: 'RS256',
expiresIn: expiration,
audience: WIKI.config.auth.audience,
issuer: 'urn:wiki.js'
})
await WIKI.db.apiKeys.query().findById(entry.id).patch({
key,
isRevoked: false
})
return key
}
}

@ -1,252 +0,0 @@
import { Model } from 'objection'
import path from 'node:path'
import fse from 'fs-extra'
import { startsWith } from 'lodash-es'
import { generateHash } from '../helpers/common.mjs'
import { Tree } from './tree.mjs'
import { User } from './users.mjs'
/**
* Users model
*/
export class Asset extends Model {
static get tableName () { return 'assets' }
static get jsonSchema () {
return {
type: 'object',
properties: {
id: { type: 'string' },
filename: { type: 'string' },
hash: { type: 'string' },
ext: { type: 'string' },
kind: { type: 'string' },
mime: { type: 'string' },
fileSize: { type: 'integer' },
metadata: { type: 'object' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' }
}
}
}
static get relationMappings () {
return {
author: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: 'assets.authorId',
to: 'users.id'
}
},
tree: {
relation: Model.HasOneRelation,
modelClass: Tree,
join: {
from: 'assets.id',
to: 'tree.id'
}
}
}
}
async $beforeUpdate (opt, context) {
await super.$beforeUpdate(opt, context)
this.updatedAt = new Date().toISOString()
}
async $beforeInsert (context) {
await super.$beforeInsert(context)
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
async getAssetPath () {
let hierarchy = []
if (this.folderId) {
hierarchy = await WIKI.db.assetFolders.getHierarchy(this.folderId)
}
return (this.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${this.filename}` : this.filename
}
async deleteAssetCache () {
await fse.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`))
}
static async upload (opts) {
const fileInfo = path.parse(opts.originalname)
// Check for existing asset
let asset = await WIKI.db.assets.query().where({
// hash: fileHash,
folderId: opts.folderId
}).first()
// Build Object
const assetRow = {
filename: opts.originalname,
ext: fileInfo.ext,
kind: startsWith(opts.mimetype, 'image/') ? 'image' : 'binary',
mime: opts.mimetype,
fileSize: opts.size,
folderId: opts.folderId
}
// Sanitize SVG contents
if (
WIKI.config.uploads.scanSVG &&
(
opts.mimetype.toLowerCase().startsWith('image/svg') ||
fileInfo.ext.toLowerCase() === '.svg'
)
) {
const svgSanitizeJob = await WIKI.scheduler.registerJob({
name: 'sanitize-svg',
immediate: true,
worker: true
}, opts.path)
await svgSanitizeJob.finished
}
// Save asset data
try {
const fileBuffer = await fse.readFile(opts.path)
if (asset) {
// Patch existing asset
if (opts.mode === 'upload') {
assetRow.authorId = opts.user.id
}
await WIKI.db.assets.query().patch(assetRow).findById(asset.id)
await WIKI.db.knex('assetData').where({
id: asset.id
}).update({
data: fileBuffer
})
} else {
// Create asset entry
assetRow.authorId = opts.user.id
asset = await WIKI.db.assets.query().insert(assetRow)
await WIKI.db.knex('assetData').insert({
id: asset.id,
data: fileBuffer
})
}
// Move temp upload to cache
// if (opts.mode === 'upload') {
// await fs.move(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
// } else {
// await fs.copy(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
// }
// Add to Storage
if (!opts.skipStorage) {
await WIKI.db.storage.assetEvent({
event: 'uploaded',
asset: {
...asset,
path: await asset.getAssetPath(),
data: fileBuffer,
authorId: opts.user.id,
authorName: opts.user.name,
authorEmail: opts.user.email
}
})
}
} catch (err) {
WIKI.logger.warn(err)
}
}
static async getThumbnail ({ id, path, locale, siteId }) {
return WIKI.db.tree.query()
.select('tree.*', 'assets.preview', 'assets.previewState')
.innerJoin('assets', 'tree.id', 'assets.id')
.where(id
? { 'tree.id': id }
: {
'tree.hash': generateHash(path),
'tree.locale': locale,
'tree.siteId': siteId
})
.first()
}
static async getAsset ({ pathArgs, siteId }, res) {
try {
const fileInfo = path.parse(pathArgs.path.toLowerCase())
const fileHash = generateHash(pathArgs.path)
const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${siteId}/${fileHash}.dat`)
// Force unsafe extensions to download
if (WIKI.config.security.forceAssetDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) {
res.set('Content-disposition', 'attachment; filename=' + encodeURIComponent(fileInfo.base))
}
if (await WIKI.db.assets.getAssetFromCache({ cachePath, extName: fileInfo.ext }, res)) {
return
}
// if (await WIKI.db.assets.getAssetFromStorage(assetPath, res)) {
// return
// }
await WIKI.db.assets.getAssetFromDb({ pathArgs, fileHash, cachePath, siteId }, res)
} catch (err) {
if (err.code === 'ECONNABORTED' || err.code === 'EPIPE') {
return
}
WIKI.logger.error(err)
res.sendStatus(500)
}
}
static async getAssetFromCache ({ cachePath, extName }, res) {
try {
await fse.access(cachePath, fse.constants.R_OK)
} catch (err) {
return false
}
res.type(extName)
await new Promise(resolve => res.sendFile(cachePath, { dotfiles: 'deny' }, resolve))
return true
}
static async getAssetFromStorage (assetPath, res) {
const localLocations = await WIKI.db.storage.getLocalLocations({
asset: {
path: assetPath
}
})
for (const location of localLocations.filter(location => Boolean(location.path))) {
const assetExists = await WIKI.db.assets.getAssetFromCache(assetPath, location.path, res)
if (assetExists) {
return true
}
}
return false
}
static async getAssetFromDb ({ pathArgs, fileHash, cachePath, siteId }, res) {
const asset = await WIKI.db.knex('tree').where({
siteId,
hash: fileHash
}).first()
if (asset) {
const assetData = await WIKI.db.knex('assets').where('id', asset.id).first()
res.type(assetData.fileExt)
res.send(assetData.data)
await fse.outputFile(cachePath, assetData.data)
} else {
res.sendStatus(404)
}
}
static async flushTempUploads () {
return fse.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
}
}

@ -1,61 +0,0 @@
import { Model } from 'objection'
import fs from 'node:fs/promises'
import path from 'node:path'
import { get } from 'lodash-es'
import yaml from 'js-yaml'
import { parseModuleProps } from '../helpers/common.mjs'
/**
* Authentication model
*/
export class Authentication extends Model {
static get tableName() { return 'authentication' }
static get jsonSchema () {
return {
type: 'object',
required: ['module'],
properties: {
id: { type: 'string' },
module: { type: 'string' },
isEnabled: { type: 'boolean' },
selfRegistration: {type: 'boolean'}
}
}
}
static get jsonAttributes() {
return ['config', 'domainWhitelist', 'autoEnrollGroups']
}
static async getStrategy(module) {
return WIKI.db.authentication.query().findOne({ module })
}
static async getStrategies({ enabledOnly = false } = {}) {
return WIKI.db.authentication.query().where(enabledOnly ? { isEnabled: true } : {})
}
static async refreshStrategiesFromDisk() {
try {
// -> Fetch definitions from disk
const authenticationDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/authentication'))
WIKI.data.authentication = []
for (const dir of authenticationDirs) {
const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/authentication', dir, 'definition.yml'), 'utf8')
const defParsed = yaml.load(def)
if (!defParsed.isAvailable) { continue }
defParsed.key = dir
defParsed.props = parseModuleProps(defParsed.props)
WIKI.data.authentication.push(defParsed)
WIKI.logger.debug(`Loaded authentication module definition ${dir}: [ OK ]`)
}
WIKI.logger.info(`Loaded ${WIKI.data.authentication.length} authentication module definitions: [ OK ]`)
} catch (err) {
WIKI.logger.error(`Failed to scan or load authentication providers: [ FAILED ]`)
WIKI.logger.error(err)
}
}
}

@ -1,28 +0,0 @@
import { Model } from 'objection'
/**
* Block model
*/
export class Block extends Model {
static get tableName () { return 'blocks' }
static get jsonAttributes () {
return ['config']
}
static async addBlock (data) {
return WIKI.db.blocks.query().insertAndFetch({
block: data.block,
name: data.name,
description: data.description,
icon: data.icon,
isEnabled: true,
isCustom: true,
config: {}
})
}
static async deleteBlock (id) {
return WIKI.db.blocks.query().deleteById(id)
}
}

@ -1,99 +0,0 @@
import { Model } from 'objection'
import fs from 'node:fs/promises'
import path from 'node:path'
import { defaultTo, find, forOwn, isBoolean, replace, sortBy } from 'lodash-es'
import yaml from 'js-yaml'
import { parseModuleProps } from '../helpers/common.mjs'
/**
* CommentProvider model
*/
export class CommentProvider extends Model {
static get tableName() { return 'commentProviders' }
static get idColumn() { return 'key' }
static get jsonSchema () {
return {
type: 'object',
required: ['key', 'isEnabled'],
properties: {
key: {type: 'string'},
isEnabled: {type: 'boolean'}
}
}
}
static get jsonAttributes() {
return ['config']
}
static async getProvider(key) {
return WIKI.db.commentProviders.query().findOne({ key })
}
static async getProviders(isEnabled) {
const providers = await WIKI.db.commentProviders.query().where(isBoolean(isEnabled) ? { isEnabled } : {})
return sortBy(providers, ['module'])
}
static async refreshProvidersFromDisk() {
try {
// -> Fetch definitions from disk
const commentsDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/comments'))
WIKI.data.commentProviders = []
for (const dir of commentsDirs) {
const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/comments', dir, 'definition.yml'), 'utf8')
const defParsed = yaml.load(def)
defParsed.key = dir
defParsed.props = parseModuleProps(defParsed.props)
WIKI.data.commentProviders.push(defParsed)
WIKI.logger.debug(`Loaded comments provider module definition ${dir}: [ OK ]`)
}
WIKI.logger.info(`Loaded ${WIKI.data.commentProviders.length} comments providers module definitions: [ OK ]`)
} catch (err) {
WIKI.logger.error(`Failed to scan or load comments providers: [ FAILED ]`)
WIKI.logger.error(err)
}
}
static async initProvider() {
const commentProvider = await WIKI.db.commentProviders.query().findOne('isEnabled', true)
if (commentProvider) {
WIKI.data.commentProvider = {
...find(WIKI.data.commentProviders, ['key', commentProvider.module]),
head: '',
bodyStart: '',
bodyEnd: '',
main: '<comments></comments>'
}
if (WIKI.data.commentProvider.codeTemplate) {
const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/comments', commentProvider.key, 'code.yml'), 'utf8')
let code = yaml.safeLoad(def)
code.head = defaultTo(code.head, '')
code.body = defaultTo(code.body, '')
code.main = defaultTo(code.main, '')
forOwn(commentProvider.config, (value, key) => {
code.head = replace(code.head, new RegExp(`{{${key}}}`, 'g'), value)
code.body = replace(code.body, new RegExp(`{{${key}}}`, 'g'), value)
code.main = replace(code.main, new RegExp(`{{${key}}}`, 'g'), value)
})
WIKI.data.commentProvider.head = code.head
WIKI.data.commentProvider.body = code.body
WIKI.data.commentProvider.main = code.main
} else {
WIKI.data.commentProvider = {
...WIKI.data.commentProvider,
...(await import(`../modules/comments/${commentProvider.key}/comment.mjs`)),
config: commentProvider.config
}
await WIKI.data.commentProvider.init()
}
WIKI.data.commentProvider.config = commentProvider.config
}
}
}

@ -1,190 +0,0 @@
import { Model } from 'objection'
import validate from 'validate.js'
import { Page } from './pages.mjs'
import { User } from './users.mjs'
/**
* Comments model
*/
export class Comment extends Model {
static get tableName() { return 'comments' }
static get jsonSchema () {
return {
type: 'object',
required: [],
properties: {
id: {type: 'integer'},
content: {type: 'string'},
render: {type: 'string'},
name: {type: 'string'},
email: {type: 'string'},
ip: {type: 'string'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
}
}
}
static get relationMappings() {
return {
author: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: 'comments.authorId',
to: 'users.id'
}
},
page: {
relation: Model.BelongsToOneRelation,
modelClass: Page,
join: {
from: 'comments.pageId',
to: 'pages.id'
}
}
}
}
$beforeUpdate() {
this.updatedAt = new Date().toISOString()
}
$beforeInsert() {
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
/**
* Post New Comment
*/
static async postNewComment ({ pageId, replyTo, content, guestName, guestEmail, user, ip }) {
// -> Input validation
if (user.id === 2) {
const validation = validate({
email: guestEmail.toLowerCase(),
name: guestName
}, {
email: {
email: true,
length: {
maximum: 255
}
},
name: {
presence: {
allowEmpty: false
},
length: {
minimum: 2,
maximum: 255
}
}
}, { format: 'flat' })
if (validation && validation.length > 0) {
throw new WIKI.Error.InputInvalid(validation[0])
}
}
content = content.trim()
if (content.length < 2) {
throw new WIKI.Error.CommentContentMissing()
}
// -> Load Page
const page = await WIKI.db.pages.getPageFromDb(pageId)
if (page) {
if (!WIKI.auth.checkAccess(user, ['write:comments'], {
path: page.path,
locale: page.locale
})) {
throw new WIKI.Error.CommentPostForbidden()
}
} else {
throw new WIKI.Error.PageNotFound()
}
// -> Process by comment provider
return WIKI.data.commentProvider.create({
page,
replyTo,
content,
user: {
...user,
...(user.id === 2) ? {
name: guestName,
email: guestEmail
} : {},
ip
}
})
}
/**
* Update an Existing Comment
*/
static async updateComment ({ id, content, user, ip }) {
// -> Load Page
const pageId = await WIKI.data.commentProvider.getPageIdFromCommentId(id)
if (!pageId) {
throw new WIKI.Error.CommentNotFound()
}
const page = await WIKI.db.pages.getPageFromDb(pageId)
if (page) {
if (!WIKI.auth.checkAccess(user, ['manage:comments'], {
path: page.path,
locale: page.locale
})) {
throw new WIKI.Error.CommentManageForbidden()
}
} else {
throw new WIKI.Error.PageNotFound()
}
// -> Process by comment provider
return WIKI.data.commentProvider.update({
id,
content,
page,
user: {
...user,
ip
}
})
}
/**
* Delete an Existing Comment
*/
static async deleteComment ({ id, user, ip }) {
// -> Load Page
const pageId = await WIKI.data.commentProvider.getPageIdFromCommentId(id)
if (!pageId) {
throw new WIKI.Error.CommentNotFound()
}
const page = await WIKI.db.pages.getPageFromDb(pageId)
if (page) {
if (!WIKI.auth.checkAccess(user, ['manage:comments'], {
path: page.path,
locale: page.locale
})) {
throw new WIKI.Error.CommentManageForbidden()
}
} else {
throw new WIKI.Error.PageNotFound()
}
// -> Process by comment provider
await WIKI.data.commentProvider.remove({
id,
page,
user: {
...user,
ip
}
})
}
}

@ -1,55 +0,0 @@
import { Model } from 'objection'
import { User } from './users.mjs'
/**
* Groups model
*/
export class Group extends Model {
static get tableName() { return 'groups' }
static get jsonSchema () {
return {
type: 'object',
required: ['name'],
properties: {
id: {type: 'integer'},
name: {type: 'string'},
isSystem: {type: 'boolean'},
redirectOnLogin: {type: 'string'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
}
}
}
static get jsonAttributes() {
return ['permissions', 'pageRules']
}
static get relationMappings() {
return {
users: {
relation: Model.ManyToManyRelation,
modelClass: User,
join: {
from: 'groups.id',
through: {
from: 'userGroups.groupId',
to: 'userGroups.userId'
},
to: 'users.id'
}
}
}
}
$beforeUpdate() {
this.updatedAt = new Date().toISOString()
}
$beforeInsert() {
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
}

@ -1,42 +0,0 @@
import { Model } from 'objection'
/**
* Hook model
*/
export class Hook extends Model {
static get tableName () { return 'hooks' }
static get jsonAttributes () {
return ['events']
}
$beforeUpdate () {
this.updatedAt = new Date()
}
static async createHook (data) {
return WIKI.db.hooks.query().insertAndFetch({
name: data.name,
events: data.events,
url: data.url,
includeMetadata: data.includeMetadata,
includeContent: data.includeContent,
acceptUntrusted: data.acceptUntrusted,
authHeader: data.authHeader,
state: 'pending',
lastErrorMessage: null
})
}
static async updateHook (id, patch) {
return WIKI.db.hooks.query().findById(id).patch({
...patch,
state: 'pending',
lastErrorMessage: null
})
}
static async deleteHook (id) {
return WIKI.db.hooks.query().deleteById(id)
}
}

@ -1,47 +0,0 @@
import { Analytics } from './analytics.mjs'
import { ApiKey } from './apiKeys.mjs'
import { Asset } from './assets.mjs'
import { Authentication } from './authentication.mjs'
import { Block } from './blocks.mjs'
import { CommentProvider } from './commentProviders.mjs'
import { Comment } from './comments.mjs'
import { Group } from './groups.mjs'
import { Hook } from './hooks.mjs'
import { Locale } from './locales.mjs'
import { Navigation } from './navigation.mjs'
import { PageHistory } from './pageHistory.mjs'
import { PageLink } from './pageLinks.mjs'
import { Page } from './pages.mjs'
import { Renderer } from './renderers.mjs'
import { Setting } from './settings.mjs'
import { Site } from './sites.mjs'
import { Storage } from './storage.mjs'
import { Tag } from './tags.mjs'
import { Tree } from './tree.mjs'
import { UserKey } from './userKeys.mjs'
import { User } from './users.mjs'
export default {
analytics: Analytics,
apiKeys: ApiKey,
assets: Asset,
authentication: Authentication,
blocks: Block,
commentProviders: CommentProvider,
comments: Comment,
groups: Group,
hooks: Hook,
locales: Locale,
navigation: Navigation,
pageHistory: PageHistory,
pageLinks: PageLink,
pages: Page,
renderers: Renderer,
settings: Setting,
sites: Site,
storage: Storage,
tags: Tag,
tree: Tree,
userKeys: UserKey,
users: User
}

@ -1,146 +0,0 @@
import { Model } from 'objection'
import { find } from 'lodash-es'
import { stat, readFile } from 'node:fs/promises'
import path from 'node:path'
import { DateTime } from 'luxon'
/**
* Locales model
*/
export class Locale extends Model {
static get tableName() { return 'locales' }
static get idColumn() { return 'code' }
static get jsonSchema () {
return {
type: 'object',
required: ['code', 'name'],
properties: {
code: {type: 'string'},
isRTL: {type: 'boolean', default: false},
name: {type: 'string'},
nativeName: {type: 'string'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'},
completeness: {type: 'integer'}
}
}
}
static get jsonAttributes() {
return ['strings']
}
$beforeUpdate() {
this.updatedAt = new Date().toISOString()
}
$beforeInsert() {
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
static async refreshFromDisk ({ force = false } = {}) {
try {
const localesMeta = (await import(`../locales/metadata.mjs`)).default
WIKI.logger.info(`Found ${localesMeta.languages.length} locales: [ OK ]`)
const dbLocales = await WIKI.db.locales.query().select('code', 'updatedAt')
let localFilesSkipped = 0
for (const lang of localesMeta.languages) {
// -> Build filename
const langFilenameParts = [lang.language]
if (lang.region) {
langFilenameParts.push(lang.region)
}
if (lang.script) {
langFilenameParts.push(lang.script)
}
const langFilename = langFilenameParts.join('-')
// -> Get DB version
const dbLang = find(dbLocales, ['code', langFilename])
// -> Get File version
const flPath = path.join(WIKI.SERVERPATH, `locales/${langFilename}.json`)
try {
const flStat = await stat(flPath)
const flUpdatedAt = DateTime.fromJSDate(flStat.mtime)
// -> Load strings
if (!dbLang || DateTime.fromJSDate(dbLang.updatedAt) < flUpdatedAt || force) {
WIKI.logger.debug(`Loading locale ${langFilename} into DB...`)
const flStrings = JSON.parse(await readFile(flPath, 'utf8'))
await WIKI.db.locales.query().insert({
code: langFilename,
name: lang.name,
nativeName: lang.localizedName,
language: lang.language,
region: lang.region,
script: lang.script,
isRTL: lang.isRtl,
strings: flStrings
}).onConflict('code').merge(['strings', 'updatedAt'])
} else {
WIKI.logger.debug(`Locale ${langFilename} is newer in the DB. Skipping disk version. [ OK ]`)
}
} catch (err) {
localFilesSkipped++
WIKI.logger.debug(`Locale ${langFilename} not found on disk. Missing strings file. [ SKIPPED ]`)
}
}
if (localFilesSkipped > 0) {
WIKI.logger.info(`${localFilesSkipped} locales were defined in the metadata file but not found on disk. [ SKIPPED ]`)
}
} catch (err) {
WIKI.logger.warn(`Failed to load locales from disk: [ FAILED ]`)
WIKI.logger.warn(err)
return false
}
}
static async getLocales ({ cache = true } = {}) {
if (!WIKI.cache.has('locales') || !cache) {
const locales = await WIKI.db.locales.query().select('code', 'isRTL', 'language', 'name', 'nativeName', 'createdAt', 'updatedAt', 'completeness')
WIKI.cache.set('locales', locales)
for (const locale of locales) {
WIKI.cache.set(`locale:${locale.code}`, locale)
}
}
return WIKI.cache.get('locales')
}
static async getStrings (locale) {
const { strings } = await WIKI.db.locales.query().findOne('code', locale).column('strings')
return strings
}
static async reloadCache () {
await WIKI.db.locales.getLocales({ cache: false })
}
static async getNavLocales({ cache = false } = {}) {
return []
// if (!WIKI.config.lang.namespacing) {
// return []
// }
// if (cache) {
// const navLocalesCached = await WIKI.cache.get('nav:locales')
// if (navLocalesCached) {
// return navLocalesCached
// }
// }
// const navLocales = await WIKI.db.locales.query().select('code', 'nativeName AS name').whereIn('code', WIKI.config.lang.namespaces).orderBy('code')
// if (navLocales) {
// if (cache) {
// await WIKI.cache.set('nav:locales', navLocales, 300)
// }
// return navLocales
// } else {
// WIKI.logger.warn('Site Locales for navigation are missing or corrupted.')
// return []
// }
}
}

@ -1,79 +0,0 @@
import { Model } from 'objection'
import { has, intersection } from 'lodash-es'
/**
* Navigation model
*/
export class Navigation extends Model {
static get tableName() { return 'navigation' }
static get jsonSchema () {
return {
type: 'object',
properties: {
name: {type: 'string'},
items: {type: 'array', items: {type: 'object'}}
}
}
}
static async getNav ({ id, cache = false, userGroups = [] }) {
const result = await WIKI.db.navigation.query().findById(id).select('items')
if (!result) { return [] }
return result.items.filter(item => {
return !item.visibilityGroups?.length || intersection(item.visibilityGroups, userGroups).length > 0
}).map(item => {
if (!item.children || item.children?.length < 1) { return item }
return {
...item,
children: item.children.filter(child => {
return !child.visibilityGroups?.length || intersection(child.visibilityGroups, userGroups).length > 0
})
}
})
}
static async getTree({ cache = false, locale = 'en', groups = [], bypassAuth = false } = {}) {
if (cache) {
const navTreeCached = await WIKI.cache.get(`nav:sidebar:${locale}`)
if (navTreeCached) {
return bypassAuth ? navTreeCached : WIKI.db.navigation.getAuthorizedItems(navTreeCached, groups)
}
}
const navTree = await WIKI.db.navigation.query().findOne('key', `site`)
if (navTree) {
// Check for pre-2.3 format
if (has(navTree.config[0], 'kind')) {
navTree.config = [{
locale: 'en',
items: navTree.config.map(item => ({
...item,
visibilityMode: 'all',
visibilityGroups: []
}))
}]
}
for (const tree of navTree.config) {
if (cache) {
await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300)
}
}
if (bypassAuth) {
return locale === 'all' ? navTree.config : WIKI.cache.get(`nav:sidebar:${locale}`)
} else {
return locale === 'all' ? WIKI.db.navigation.getAuthorizedItems(navTree.config, groups) : WIKI.db.navigation.getAuthorizedItems(WIKI.cache.get(`nav:sidebar:${locale}`), groups)
}
} else {
WIKI.logger.warn('Site Navigation is missing or corrupted.')
return []
}
}
static getAuthorizedItems(tree = [], groups = []) {
return tree.filter(leaf => {
return leaf.visibilityMode === 'all' || intersection(leaf.visibilityGroups, groups).length > 0
})
}
}

@ -1,238 +0,0 @@
import { Model } from 'objection'
import { get, reduce, reverse } from 'lodash-es'
import { DateTime, Duration } from 'luxon'
import { Locale } from './locales.mjs'
import { Page } from './pages.mjs'
import { User } from './users.mjs'
import { Tag } from './tags.mjs'
/**
* Page History model
*/
export class PageHistory extends Model {
static get tableName() { return 'pageHistory' }
static get jsonSchema () {
return {
type: 'object',
required: ['path', 'title'],
properties: {
id: {type: 'integer'},
path: {type: 'string'},
hash: {type: 'string'},
title: {type: 'string'},
description: {type: 'string'},
publishState: {type: 'string'},
publishStartDate: {type: 'string'},
publishEndDate: {type: 'string'},
content: {type: 'string'},
contentType: {type: 'string'},
createdAt: {type: 'string'}
}
}
}
static get relationMappings() {
return {
tags: {
relation: Model.ManyToManyRelation,
modelClass: Tag,
join: {
from: 'pageHistory.id',
through: {
from: 'pageHistoryTags.pageId',
to: 'pageHistoryTags.tagId'
},
to: 'tags.id'
}
},
page: {
relation: Model.BelongsToOneRelation,
modelClass: Page,
join: {
from: 'pageHistory.pageId',
to: 'pages.id'
}
},
author: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: 'pageHistory.authorId',
to: 'users.id'
}
}
}
}
$beforeInsert() {
this.createdAt = new Date().toISOString()
}
/**
* Create Page Version
*/
static async addVersion(opts) {
await WIKI.db.pageHistory.query().insert({
action: opts.historyData?.action ?? 'updated',
affectedFields: JSON.stringify(opts.historyData?.affectedFields ?? []),
alias: opts.alias,
config: JSON.stringify(opts.config ?? {}),
authorId: opts.authorId,
content: opts.content,
contentType: opts.contentType,
description: opts.description,
editor: opts.editor,
hash: opts.hash,
icon: opts.icon,
locale: opts.locale,
pageId: opts.id,
path: opts.path,
publishEndDate: opts.publishEndDate?.toISO(),
publishStartDate: opts.publishStartDate?.toISO(),
publishState: opts.publishState,
reason: opts.historyData?.reason,
relations: JSON.stringify(opts.relations ?? []),
render: opts.render,
scripts: JSON.stringify(opts.scripts ?? {}),
siteId: opts.siteId,
title: opts.title,
toc: JSON.stringify(opts.toc ?? []),
versionDate: opts.versionDate
})
}
/**
* Get Page Version
*/
static async getVersion({ pageId, versionId }) {
const version = await WIKI.db.pageHistory.query()
.column([
'pageHistory.path',
'pageHistory.title',
'pageHistory.description',
'pageHistory.isPublished',
'pageHistory.publishStartDate',
'pageHistory.publishEndDate',
'pageHistory.content',
'pageHistory.contentType',
'pageHistory.createdAt',
'pageHistory.action',
'pageHistory.authorId',
'pageHistory.pageId',
'pageHistory.versionDate',
{
versionId: 'pageHistory.id',
editor: 'pageHistory.editorKey',
locale: 'pageHistory.locale',
authorName: 'author.name'
}
])
.joinRelated('author')
.where({
'pageHistory.id': versionId,
'pageHistory.pageId': pageId
}).first()
if (version) {
return {
...version,
updatedAt: version.createdAt || null,
tags: []
}
} else {
return null
}
}
/**
* Get History Trail of a Page
*/
static async getHistory({ pageId, offsetPage = 0, offsetSize = 100 }) {
const history = await WIKI.db.pageHistory.query()
.column([
'pageHistory.id',
'pageHistory.path',
'pageHistory.authorId',
'pageHistory.action',
'pageHistory.versionDate',
{
authorName: 'author.name'
}
])
.joinRelated('author')
.where({
'pageHistory.pageId': pageId
})
.orderBy('pageHistory.versionDate', 'desc')
.page(offsetPage, offsetSize)
let prevPh = null
const upperLimit = (offsetPage + 1) * offsetSize
if (history.total >= upperLimit) {
prevPh = await WIKI.db.pageHistory.query()
.column([
'pageHistory.id',
'pageHistory.path',
'pageHistory.authorId',
'pageHistory.action',
'pageHistory.versionDate',
{
authorName: 'author.name'
}
])
.joinRelated('author')
.where({
'pageHistory.pageId': pageId
})
.orderBy('pageHistory.versionDate', 'desc')
.offset((offsetPage + 1) * offsetSize)
.limit(1)
.first()
}
return {
trail: reduce(reverse(history.results), (res, ph) => {
let actionType = 'edit'
let valueBefore = null
let valueAfter = null
if (!prevPh && history.total < upperLimit) {
actionType = 'initial'
} else if (get(prevPh, 'path', '') !== ph.path) {
actionType = 'move'
valueBefore = get(prevPh, 'path', '')
valueAfter = ph.path
}
res.unshift({
versionId: ph.id,
authorId: ph.authorId,
authorName: ph.authorName,
actionType,
valueBefore,
valueAfter,
versionDate: ph.versionDate
})
prevPh = ph
return res
}, []),
total: history.total
}
}
/**
* Purge history older than X
*
* @param {String} olderThan ISO 8601 Duration
*/
static async purge (olderThan) {
const dur = Duration.fromISO(olderThan)
const olderThanISO = DateTime.utc().minus(dur)
await WIKI.db.pageHistory.query().where('versionDate', '<', olderThanISO.toISO()).del()
}
}

@ -1,36 +0,0 @@
import { Model } from 'objection'
import { Page } from './pages.mjs'
/**
* Users model
*/
export class PageLink extends Model {
static get tableName() { return 'pageLinks' }
static get jsonSchema () {
return {
type: 'object',
required: ['path', 'locale'],
properties: {
id: {type: 'integer'},
path: {type: 'string'},
locale: {type: 'string'}
}
}
}
static get relationMappings() {
return {
page: {
relation: Model.BelongsToOneRelation,
modelClass: Page,
join: {
from: 'pageLinks.pageId',
to: 'pages.id'
}
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,174 +0,0 @@
import { Model } from 'objection'
import path from 'node:path'
import fs from 'fs/promises'
import { clone, filter, find, get, has, reverse, some, transform, union } from 'lodash-es'
import yaml from 'js-yaml'
import { DepGraph } from 'dependency-graph'
import { parseModuleProps } from '../helpers/common.mjs'
/**
* Renderer model
*/
export class Renderer extends Model {
static get tableName() { return 'renderers' }
static get jsonSchema () {
return {
type: 'object',
required: ['module', 'isEnabled'],
properties: {
id: {type: 'string'},
module: {type: 'string'},
isEnabled: {type: 'boolean'}
}
}
}
static get jsonAttributes() {
return ['config']
}
static async getRenderers() {
return WIKI.db.renderers.query()
}
static async fetchDefinitions() {
try {
// -> Fetch definitions from disk
const renderersDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/rendering'))
WIKI.data.renderers = []
for (const dir of renderersDirs) {
const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/rendering', dir, 'definition.yml'), 'utf8')
const defParsed = yaml.load(def)
defParsed.key = dir
defParsed.props = parseModuleProps(defParsed.props)
WIKI.data.renderers.push(defParsed)
WIKI.logger.debug(`Loaded renderers module definition ${dir}: [ OK ]`)
}
WIKI.logger.info(`Loaded ${WIKI.data.renderers.length} renderers module definitions: [ OK ]`)
} catch (err) {
WIKI.logger.error(`Failed to scan or load renderers providers: [ FAILED ]`)
WIKI.logger.error(err)
}
}
static async refreshRenderersFromDisk() {
try {
const dbRenderers = await WIKI.db.renderers.query()
// -> Fetch definitions from disk
await WIKI.db.renderers.fetchDefinitions()
// -> Insert new Renderers
const newRenderers = []
let updatedRenderers = 0
for (const renderer of WIKI.data.renderers) {
if (!some(dbRenderers, ['module', renderer.key])) {
newRenderers.push({
module: renderer.key,
isEnabled: renderer.enabledDefault ?? true,
config: transform(renderer.props, (result, value, key) => {
result[key] = value.default
return result
}, {})
})
} else {
const rendererConfig = get(find(dbRenderers, ['module', renderer.key]), 'config', {})
await WIKI.db.renderers.query().patch({
config: transform(renderer.props, (result, value, key) => {
if (!has(result, key)) {
result[key] = value.default
}
return result
}, rendererConfig)
}).where('module', renderer.key)
updatedRenderers++
}
}
if (newRenderers.length > 0) {
await WIKI.db.renderers.query().insert(newRenderers)
WIKI.logger.info(`Loaded ${newRenderers.length} new renderers: [ OK ]`)
}
if (updatedRenderers > 0) {
WIKI.logger.info(`Updated ${updatedRenderers} existing renderers: [ OK ]`)
}
// -> Delete removed Renderers
for (const renderer of dbRenderers) {
if (!some(WIKI.data.renderers, ['key', renderer.module])) {
await WIKI.db.renderers.query().where('module', renderer.module).del()
WIKI.logger.info(`Removed renderer ${renderer.module} because it is no longer present in the modules folder: [ OK ]`)
}
}
} catch (err) {
WIKI.logger.error('Failed to import renderers: [ FAILED ]')
WIKI.logger.error(err)
}
}
static async getRenderingPipeline(contentType) {
const renderersDb = await WIKI.db.renderers.query().where('isEnabled', true)
if (renderersDb && renderersDb.length > 0) {
const renderers = renderersDb.map(rdr => {
const renderer = find(WIKI.data.renderers, ['key', rdr.module])
return {
...renderer,
config: rdr.config
}
})
// Build tree
const rawCores = filter(renderers, renderer => !has(renderer, 'dependsOn')).map(core => {
core.children = filter(renderers, ['dependsOn', core.key])
return core
})
// Build dependency graph
const graph = new DepGraph({ circular: true })
rawCores.map(core => { graph.addNode(core.key) })
rawCores.map(core => {
rawCores.map(coreTarget => {
if (core.key !== coreTarget.key) {
if (core.output === coreTarget.input) {
graph.addDependency(core.key, coreTarget.key)
}
}
})
})
// Filter unused cores
let activeCoreKeys = filter(rawCores, ['input', contentType]).map(core => core.key)
clone(activeCoreKeys).map(coreKey => {
activeCoreKeys = union(activeCoreKeys, graph.dependenciesOf(coreKey))
})
const activeCores = filter(rawCores, core => activeCoreKeys.includes(core.key))
// Rebuild dependency graph with active cores
const graphActive = new DepGraph({ circular: true })
activeCores.map(core => { graphActive.addNode(core.key) })
activeCores.map(core => {
activeCores.map(coreTarget => {
if (core.key !== coreTarget.key) {
if (core.output === coreTarget.input) {
graphActive.addDependency(core.key, coreTarget.key)
}
}
})
})
// Reorder cores in reverse dependency order
let orderedCores = []
reverse(graphActive.overallOrder()).map(coreKey => {
orderedCores.push(find(rawCores, ['key', coreKey]))
})
return orderedCores
} else {
WIKI.logger.error(`Rendering pipeline is empty!`)
return false
}
}
}

@ -1,37 +0,0 @@
import { Model } from 'objection'
import { has, reduce, set } from 'lodash-es'
/**
* Settings model
*/
export class Setting extends Model {
static get tableName() { return 'settings' }
static get idColumn() { return 'key' }
static get jsonSchema () {
return {
type: 'object',
required: ['key'],
properties: {
key: {type: 'string'}
}
}
}
static get jsonAttributes() {
return ['value']
}
static async getConfig() {
const settings = await WIKI.db.settings.query()
if (settings.length > 0) {
return reduce(settings, (res, val, key) => {
set(res, val.key, (has(val.value, 'v')) ? val.value.v : val.value)
return res
}, {})
} else {
return false
}
}
}

@ -1,188 +0,0 @@
import { Model } from 'objection'
import { defaultsDeep, keyBy } from 'lodash-es'
import { v4 as uuid } from 'uuid'
/**
* Site model
*/
export class Site extends Model {
static get tableName () { return 'sites' }
static get jsonSchema () {
return {
type: 'object',
required: ['hostname'],
properties: {
id: { type: 'string' },
hostname: { type: 'string' },
isEnabled: { type: 'boolean', default: false }
}
}
}
static get jsonAttributes () {
return ['config']
}
static async getSiteByHostname ({ hostname, forceReload = false }) {
if (forceReload) {
await WIKI.db.sites.reloadCache()
}
const siteId = WIKI.sitesMappings[hostname] || WIKI.sitesMappings['*']
if (siteId) {
return WIKI.sites[siteId]
}
return null
}
static async reloadCache () {
WIKI.logger.info('Reloading site configurations...')
const sites = await WIKI.db.sites.query().orderBy('id')
WIKI.sites = keyBy(sites, 'id')
WIKI.sitesMappings = {}
for (const site of sites) {
WIKI.sitesMappings[site.hostname] = site.id
}
WIKI.logger.info(`Loaded ${sites.length} site configurations [ OK ]`)
}
static async createSite (hostname, config) {
const newSite = await WIKI.db.sites.query().insertAndFetch({
hostname,
isEnabled: true,
config: defaultsDeep(config, {
title: 'My Wiki Site',
description: '',
company: '',
contentLicense: '',
footerExtra: '',
pageExtensions: ['md', 'html', 'txt'],
pageCasing: true,
discoverable: false,
defaults: {
tocDepth: {
min: 1,
max: 2
}
},
features: {
browse: true,
ratings: false,
ratingsMode: 'off',
comments: false,
contributions: false,
profile: true,
search: true
},
logoUrl: '',
logoText: true,
sitemap: true,
robots: {
index: true,
follow: true
},
locales: {
primary: 'en',
active: ['en']
},
assets: {
logo: false,
logoExt: 'svg',
favicon: false,
faviconExt: 'svg',
loginBg: false
},
theme: {
dark: false,
codeBlocksTheme: 'github-dark',
colorPrimary: '#1976D2',
colorSecondary: '#02C39A',
colorAccent: '#FF9800',
colorHeader: '#000000',
colorSidebar: '#1976D2',
injectCSS: '',
injectHead: '',
injectBody: '',
contentWidth: 'full',
sidebarPosition: 'left',
tocPosition: 'right',
showSharingMenu: true,
showPrintBtn: true,
baseFont: 'roboto',
contentFont: 'roboto'
},
editors: {
asciidoc: {
isActive: true,
config: {}
},
markdown: {
isActive: true,
config: {
allowHTML: true,
kroki: false,
krokiServerUrl: 'https://kroki.io',
latexEngine: 'katex',
lineBreaks: true,
linkify: true,
multimdTable: true,
plantuml: false,
plantumlServerUrl: 'https://www.plantuml.com/plantuml/',
quotes: 'english',
tabWidth: 2,
typographer: false,
underline: true
}
},
wysiwyg: {
isActive: true,
config: {}
}
},
uploads: {
conflictBehavior: 'overwrite',
normalizeFilename: true
}
})
})
WIKI.logger.debug(`Creating new root navigation for site ${newSite.id}`)
await WIKI.db.navigation.query().insert({
id: newSite.id,
siteId: newSite.id,
items: []
})
WIKI.logger.debug(`Creating new DB storage for site ${newSite.id}`)
await WIKI.db.storage.query().insert({
module: 'db',
siteId: newSite.id,
isEnabled: true,
contentTypes: {
activeTypes: ['pages', 'images', 'documents', 'others', 'large'],
largeThreshold: '5MB'
},
assetDelivery: {
streaming: true,
directAccess: false
},
state: {
current: 'ok'
}
})
return newSite
}
static async updateSite (id, patch) {
return WIKI.db.sites.query().findById(id).patch(patch)
}
static async deleteSite (id) {
await WIKI.db.storage.query().delete().where('siteId', id)
return WIKI.db.sites.query().deleteById(id)
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save