Merge branch 'dynamic-toc' of https://github.com/YunusEmreAlps/wiki into dynamic-toc

pull/6052/head
Yunus Emre Alpu 21 hours ago
commit 4ba0a7ce42

@ -1,12 +0,0 @@
<!-- Wiki.js 1.x? Go to https://github.com/Requarks/wiki-v1/issues -->
### Actual behavior
### Expected behavior
### Steps to reproduce the behavior
<!-- FOR FEATURE REQUESTS: Use the feature request board instead: https://requests.requarks.io/wiki -->
<!-- Love Wiki.js? Please consider supporting our collective:
👉 https://opencollective.com/wikijs/donate -->

@ -3,9 +3,6 @@ contact_links:
- name: Help / Questions
url: https://github.com/Requarks/wiki/discussions/categories/help-questions
about: Ask the community for help on using or setting up Wiki.js
- name: Report a Security Issue
url: https://github.com/Requarks/wiki/security/policy
about: Privately report security issues so they can be addressed quickly.
- name: Errors / Bug Reports
url: https://github.com/Requarks/wiki/discussions/categories/error-bug-report
about: Create a discussion around the bug / error you're getting. If validated, a proper GitHub issue will be created so that it can be worked on.

@ -19,7 +19,7 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- name: Set Build Variables
run: |
@ -42,20 +42,22 @@ jobs:
cat package.json
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images
uses: docker/build-push-action@v2.9.0
uses: docker/build-push-action@v7
env:
DOCKER_BUILD_SUMMARY: false
with:
context: .
file: dev/build/Dockerfile
@ -77,7 +79,7 @@ jobs:
find _dist/wiki/ -printf "%P\n" | tar -czf wiki-js.tar.gz --no-recursion -C _dist/wiki/ -T -
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v7
with:
name: drop
path: wiki-js.tar.gz
@ -89,10 +91,10 @@ jobs:
strategy:
matrix:
dbtype: [postgres, mysql, mariadb, mssql, sqlite]
dbtype: [postgres, mysql, mariadb, sqlite]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- name: Set Test Variables
run: |
@ -115,21 +117,13 @@ jobs:
arm:
name: ARM Build
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
needs: [cypress]
permissions:
packages: write
strategy:
matrix:
include:
- platform: linux/arm64
docker: arm64
- platform: linux/arm/v7
docker: armv7
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- name: Set Version Variables
run: |
@ -142,26 +136,26 @@ jobs:
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download a Build Artifact
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v8
with:
name: drop
path: drop
@ -172,15 +166,18 @@ jobs:
tar -xzf $GITHUB_WORKSPACE/drop/wiki-js.tar.gz -C $GITHUB_WORKSPACE/build --exclude=node_modules
- name: Build and push Docker images
uses: docker/build-push-action@v2.9.0
uses: docker/build-push-action@v7
env:
DOCKER_BUILD_SUMMARY: false
with:
context: .
file: dev/build-arm/Dockerfile
platforms: ${{ matrix.platform }}
platforms: linux/arm64
provenance: false
push: true
tags: |
requarks/wiki:canary-${{ matrix.docker }}-${{ env.REL_VERSION_STRICT }}
ghcr.io/requarks/wiki:canary-${{ matrix.docker }}-${{ env.REL_VERSION_STRICT }}
requarks/wiki:canary-arm64-${{ env.REL_VERSION_STRICT }}
ghcr.io/requarks/wiki:canary-arm64-${{ env.REL_VERSION_STRICT }}
windows:
name: Windows Build
@ -189,12 +186,12 @@ jobs:
steps:
- name: Setup Node.js environment
uses: actions/setup-node@v2.5.1
uses: actions/setup-node@v6
with:
node-version: 16.x
node-version: 24.x
- name: Download a Build Artifact
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v8
with:
name: drop
path: drop
@ -202,17 +199,25 @@ jobs:
- name: Extract Build
run: |
mkdir -p win
tar -xzf $env:GITHUB_WORKSPACE\drop\wiki-js.tar.gz -C $env:GITHUB_WORKSPACE\win --exclude=node_modules
tar -xzf $env:GITHUB_WORKSPACE\drop\wiki-js.tar.gz -C $env:GITHUB_WORKSPACE\win
Copy-Item win\node_modules\extract-files\package.json patch-extractfile.json -Force
Remove-Item -Path win\node_modules -Force -Recurse
- name: Install Dependencies
run: yarn --production --frozen-lockfile --non-interactive
run: |
yarn --production --frozen-lockfile --non-interactive
yarn patch-package
working-directory: win
- name: Fix patched packages
run: |
Copy-Item patch-extractfile.json win\node_modules\extract-files\package.json -Force
- name: Create Bundle
run: tar -czf wiki-js-windows.tar.gz -C $env:GITHUB_WORKSPACE\win .
run: tar -czf $env:GITHUB_WORKSPACE\wiki-js-windows.tar.gz -C $env:GITHUB_WORKSPACE\win .
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v7
with:
name: drop-win
path: wiki-js-windows.tar.gz
@ -232,13 +237,13 @@ jobs:
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -248,8 +253,8 @@ jobs:
run: |
echo "Creating the manifests..."
docker manifest create requarks/wiki:beta-$REL_VERSION_STRICT requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:beta-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create requarks/wiki:beta-$REL_VERSION_STRICT requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:beta-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT
echo "Pushing the manifests..."
@ -273,13 +278,13 @@ jobs:
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -298,14 +303,14 @@ jobs:
echo "Using major $MAJOR and minor $MINOR..."
echo "Creating the manifests..."
docker manifest create requarks/wiki:$REL_VERSION_STRICT requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create requarks/wiki:$MAJOR requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create requarks/wiki:$MAJORMINOR requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create requarks/wiki:latest requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:$MAJOR ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:$MAJORMINOR ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:latest ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-armv7-$REL_VERSION_STRICT
docker manifest create requarks/wiki:$REL_VERSION_STRICT requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT
docker manifest create requarks/wiki:$MAJOR requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT
docker manifest create requarks/wiki:$MAJORMINOR requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT
docker manifest create requarks/wiki:latest requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:$MAJOR ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:$MAJORMINOR ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT
docker manifest create ghcr.io/requarks/wiki:latest ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT
echo "Pushing the manifests..."
@ -319,13 +324,13 @@ jobs:
docker manifest push -p ghcr.io/requarks/wiki:latest
- name: Download Linux Build
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v8
with:
name: drop
path: drop
- name: Download Windows Build
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v8
with:
name: drop-win
path: drop-win
@ -339,28 +344,29 @@ jobs:
writeToFile: false
- name: Update GitHub Release
uses: ncipollo/release-action@v1
uses: ncipollo/release-action@v1.21.0
with:
allowUpdates: true
draft: false
makeLatest: true
name: ${{ github.ref_name }}
body: ${{ steps.changelog.outputs.changes }}
token: ${{ github.token }}
artifacts: 'drop/wiki-js.tar.gz,drop-win/wiki-js-windows.tar.gz'
- name: Notify Slack Releases Channel
uses: slackapi/slack-github-action@v1.18.0
with:
payload: |
{
"text": "Wiki.js ${{ github.ref_name }} has been released."
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
# - name: Notify Slack Releases Channel
# uses: slackapi/slack-github-action@v1.26.0
# with:
# payload: |
# {
# "text": "Wiki.js ${{ github.ref_name }} has been released."
# }
# env:
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
# SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: Notify Telegram Channel
uses: appleboy/telegram-action@v0.1.1
uses: appleboy/telegram-action@v1.0.1
with:
to: ${{ secrets.TELEGRAM_TO }}
token: ${{ secrets.TELEGRAM_TOKEN }}
@ -371,34 +377,34 @@ jobs:
See [release notes](https://github.com/requarks/wiki/releases) for details.
- name: Notify Discord Channel
uses: sebastianpopp/discord-action@v1.0
uses: sebastianpopp/discord-action@v2.0
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
message: Wiki.js ${{ github.ref_name }} has been released! See https://github.com/requarks/wiki/releases for details.
build-do-image:
name: Build DigitalOcean Image
runs-on: ubuntu-latest
needs: [release]
steps:
- uses: actions/checkout@v2
- name: Set Version Variables
run: |
echo "Using TAG mode: $GITHUB_REF_NAME"
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Install Packer
run: |
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install packer
- name: Build Droplet Image
env:
DIGITALOCEAN_API_TOKEN: ${{ secrets.DO_TOKEN }}
WIKI_APP_VERSION: ${{ env.REL_VERSION_STRICT }}
working-directory: dev/packer
run: |
packer build digitalocean.json
# build-do-image:
# name: Build DigitalOcean Image
# runs-on: ubuntu-latest
# needs: [release]
# steps:
# - uses: actions/checkout@v6
# - name: Set Version Variables
# run: |
# echo "Using TAG mode: $GITHUB_REF_NAME"
# echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
# - name: Install Packer
# run: |
# curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
# sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
# sudo apt-get update && sudo apt-get install packer
# - name: Build Droplet Image
# env:
# DIGITALOCEAN_API_TOKEN: ${{ secrets.DO_TOKEN }}
# WIKI_APP_VERSION: ${{ env.REL_VERSION_STRICT }}
# working-directory: dev/packer
# run: |
# packer build digitalocean.json

@ -16,11 +16,12 @@ jobs:
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- name: Package and Push Chart
run: |
export CHARTVER=$(yq '.version' dev/helm/Chart.yaml)
helm plugin install https://github.com/chartmuseum/helm-push.git
helm repo add chartmuseum https://charts.js.wiki
helm cm-push --version="2.2.${{github.run_number}}" --username="${{secrets.HELM_REPO_USERNAME}}" --password="${{secrets.HELM_REPO_PASSWORD}}" dev/helm/ chartmuseum
helm cm-push --version="$CHARTVER" --username="${{secrets.HELM_REPO_USERNAME}}" --password="${{secrets.HELM_REPO_PASSWORD}}" dev/helm/ chartmuseum
helm repo remove chartmuseum

@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- name: Install Packer
run: |
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install packer
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install packer
- name: Build Droplet Image
env:
@ -28,4 +28,5 @@ jobs:
WIKI_APP_VERSION: ${{ github.event.inputs.version }}
working-directory: dev/packer
run: |
packer plugins install github.com/digitalocean/digitalocean
packer build digitalocean.json

@ -1 +1 @@
v12.16.3
v24.12.0

@ -8,7 +8,7 @@
"vue"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"i18n-ally.localesPaths": [
"server/locales"

@ -1,21 +1,21 @@
<div align="center">
<img src="https://static.requarks.io/logo/wikijs-full.svg" alt="Wiki.js" width="600" />
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://static.requarks.io/logo/wikijs-full-darktheme.svg">
<img alt="Wiki.js" src="https://static.requarks.io/logo/wikijs-full.svg" width="600">
</picture>
[![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat&maxAge=3600)](https://github.com/Requarks/wiki/releases)
[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat)](https://github.com/requarks/wiki/blob/master/LICENSE)
[![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-green.svg?style=flat&logo=javascript&logoColor=white)](http://standardjs.com/)
[![Downloads](https://img.shields.io/github/downloads/Requarks/wiki/total.svg?style=flat&logo=github)](https://github.com/Requarks/wiki/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/requarks/wiki.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/requarks/wiki/)
[![Build + Publish](https://github.com/Requarks/wiki/actions/workflows/build.yml/badge.svg)](https://github.com/Requarks/wiki/actions/workflows/build.yml)
[![Huntr](https://img.shields.io/badge/security%20bounty-disclose-brightgreen.svg?style=flat&logo=cachet&logoColor=white)](https://huntr.dev/bounties/disclose)
[![Build + Publish](https://github.com/Requarks/wiki/actions/workflows/build.yml/badge.svg)](https://github.com/Requarks/wiki/actions/workflows/build.yml)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/ngpixel?logo=github&color=ea4aaa)](https://github.com/users/NGPixel/sponsorship)
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs)
[![Subscribe to Newsletter](https://img.shields.io/badge/newsletter-subscribe-yellow.svg?style=flat&logo=mailchimp&logoColor=white)](https://blog.js.wiki/subscribe)
[![Chat on Slack](https://img.shields.io/badge/slack-requarks-CC2B5E.svg?style=flat&logo=slack)](https://wiki.requarks.io/slack)
[![Follow on Twitter](https://img.shields.io/badge/twitter-%40requarks-blue.svg?style=flat&logo=twitter&logoColor=white)](https://twitter.com/requarks)
[![Follow on Telegram](https://img.shields.io/badge/telegram-%40wiki__js-blue.svg?style=flat&logo=telegram)](https://t.me/wiki_js)
[![Downloads](https://img.shields.io/github/downloads/Requarks/wiki/total.svg?style=flat&logo=github)](https://github.com/Requarks/wiki/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/requarks/wiki.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/requarks/wiki/)
[![Chat on Discord](https://img.shields.io/badge/discord-join-8D96F6.svg?style=flat&logo=discord&logoColor=white)](https://discord.gg/rcxt9QS2jd)
[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40js.wiki-blue.svg?style=flat&logo=bluesky&logoColor=white)](https://bsky.app/profile/js.wiki)
[![Follow on Telegram](https://img.shields.io/badge/telegram-%40wiki__js-blue.svg?style=flat&logo=telegram)](https://t.me/wiki_js)
[![Reddit](https://img.shields.io/badge/reddit-%2Fr%2Fwikijs-orange?logo=reddit&logoColor=white)](https://www.reddit.com/r/wikijs/)
##### A modern, lightweight and powerful wiki app built on NodeJS
@ -29,9 +29,8 @@
- [Demo](https://docs.requarks.io/demo)
- [Changelog](https://github.com/requarks/wiki/releases)
- [Feature Requests](https://feedback.js.wiki/wiki)
- [Chat with us on Slack](https://wiki.requarks.io/slack)
- Chat with us on [Discord](https://discord.gg/rcxt9QS2jd)
- [Translations](https://docs.requarks.io/dev/translations) *(We need your help!)*
- [E2E Testing Results](https://dashboard.cypress.io/projects/r7qxah/runs)
- [Special Thanks](#special-thanks)
- [Contribute](#contributors)
@ -82,11 +81,6 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
<img src="https://cdn.js.wiki/images/sponsors/stellarhosted.png">
</a>
</td>
<td align="center" valign="middle" width="444">
<a href="https://www.hostwiki.com/" target="_blank">
<img src="https://cdn.js.wiki/images/sponsors/hostwiki.png">
</a>
</td>
</tr>
</tbody>
</table>
@ -96,53 +90,74 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
<table>
<tbody>
<tr>
<td align="center" valign="middle" width="148">
<td align="center" valign="middle" width="130">
<a href="https://acceleanation.com/" target="_blank">
<img src="https://avatars.githubusercontent.com/u/41210718?s=200&v=4">
</a>
</td>
<td align="center" valign="middle" width="130">
<a href="https://github.com/alexksso" target="_blank">
Alexander Casassovici<br />(@alexksso)
</a>
</td>
<td align="center" valign="middle" width="148">
<td align="center" valign="middle" width="130">
<a href="https://github.com/broxen" target="_blank">
Broxen<br />(@broxen)
</a>
</td>
<td align="center" valign="middle" width="148">
<td align="center" valign="middle" width="130">
<a href="https://github.com/xDacon" target="_blank">
Dacon<br />(@xDacon)
</a>
</td>
<td align="center" valign="middle" width="148">
<td align="center" valign="middle" width="130">
<a href="https://github.com/DonNabla" target="_blank">
Maxime Pierre<br />(@DonNabla)
</a>
</td>
<td align="center" valign="middle" width="130">
<a href="https://github.com/GigabiteLabs" target="_blank">
<img src="https://static.requarks.io/sponsors/gigabitelabs-148x129.png">
</a>
</td>
<td align="center" valign="middle" width="148">
<td align="center" valign="middle" width="130">
<a href="https://www.hostwiki.com/" target="_blank">
<img src="https://cdn.js.wiki/images/sponsors/hostwiki.png">
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle" width="130">
<a href="https://github.com/JayDaley" target="_blank">
Jay Daley<br />(@JayDaley)
</a>
</td>
<td align="center" valign="middle" width="148">
<td align="center" valign="middle" width="130">
<a href="https://github.com/idokka" target="_blank">
Oleksii<br />(@idokka)
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle" width="148">
<td align="center" valign="middle" width="130">
<a href="https://www.openhost-network.com/" target="_blank">
<img src="https://avatars.githubusercontent.com/u/114218287?s=200&v=4">
</a>
</td>
<td align="center" valign="middle" width="148">
<td align="center" valign="middle" width="130">
<a href="https://www.prevo.ch/" target="_blank">
<img src="https://avatars.githubusercontent.com/u/114394792?v=4">
</a>
</td>
<td align="center" valign="middle" colspan="4">
<a href="https://github.com/sponsors/NGPixel" target="_blank">
<img src="https://static.requarks.io/sponsors/become-148x72.png">
<td align="center" valign="middle" width="130">
<a href="https://github.com/shanekearney" target="_blank">
Shane Kearney<br />(@shanekearney)
</a>
</td>
<td align="center" valign="middle" width="130">
<a href="http://www.taicep.org/" target="_blank">
<img src="https://avatars.githubusercontent.com/u/160072306?v=4">
</a>
</td>
<td align="center" valign="middle" width="130"></td>
</tr>
</tbody>
</table>
@ -160,9 +175,11 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
- Cloud Data Hosting LLC ([@CloudDataHostingLLC](https://github.com/CloudDataHostingLLC))
- Cole Manning ([@RVRX](https://github.com/RVRX))
- CrazyMarvin ([@CrazyMarvin](https://github.com/CrazyMarvin))
- Daniel Horner ([@danhorner](https://github.com/danhorner))
- David Christian Holin ([@SirGibihm](https://github.com/SirGibihm))
- Dragan Espenschied ([@despens](https://github.com/despens))
- Elijah Zobenko ([@he110](https://github.com/he110))
- Emerson-Perna ([@Emerson-Perna](https://github.com/Emerson-Perna))
- Ernie ([@iamernie](https://github.com/iamernie))
- Fabio Ferrari ([@devxops](https://github.com/devxops))
- Finsa S.p.A. ([@finsaspa](https://github.com/finsaspa))
@ -178,33 +195,40 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
- MaFarine ([@MaFarine](https://github.com/MaFarine))
- Marcilio Leite Neto ([@marclneto](https://github.com/marclneto))
- Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson))
</td><td>
<img width="441" height="1" />
- Max Ricketts-Uy ([@MaxRickettsUy](https://github.com/MaxRickettsUy))
- Mickael Asseline ([@PAPAMICA](https://github.com/PAPAMICA))
- Mitchell Rowton ([@mrowton](https://github.com/mrowton))
</td><td>
<img width="441" height="1" />
- M. Scott Ford ([@mscottford](https://github.com/mscottford))
- Nick Halase ([@nhalase](https://github.com/nhalase))
- Nick Price ([@DominoTree](https://github.com/DominoTree))
- Nina Reynolds ([@cutecycle](https://github.com/cutecycle))
- Noel Cower ([@nilium](https://github.com/nilium))
- Oleksandr Koltsov ([@crambo](https://github.com/crambo))
- Phi Zeroth ([@phizeroth](https://github.com/phizeroth))
- Philipp Schmitt ([@pschmitt](https://github.com/pschmitt))
- Robert Lanzke ([@winkelement](https://github.com/winkelement))
- Ruizhe Li ([@liruizhe1995](https://github.com/liruizhe1995))
- Sam Martin ([@ABitMoreDepth](https://github.com/ABitMoreDepth))
- Sean Coffey ([@seanecoffey](https://github.com/seanecoffey))
- Simon Ott ([@ottsimon](https://github.com/ottsimon))
- Stephan Kristyn ([@stevek-pro](https://github.com/stevek-pro))
- Theodore Chu ([@TheodoreChu](https://github.com/TheodoreChu))
- Tim Elmer ([@tim-elmer](https://github.com/tim-elmer))
- Tyler Denman ([@tylerguy](https://github.com/tylerguy))
- Victor Bilgin ([@vbilgin](https://github.com/vbilgin))
- VMO Solutions ([@vmosolutions](https://github.com/vmosolutions))
- YazMogg35 ([@YazMogg35](https://github.com/YazMogg35))
- Yu Yongwoo ([@uyu423](https://github.com/uyu423))
- ameyrakheja ([@ameyrakheja](https://github.com/ameyrakheja))
- aniketpanjwani ([@aniketpanjwani](https://github.com/aniketpanjwani))
- aytaa ([@aytaa](https://github.com/aytaa))
- cesar ([@cesarnr21](https://github.com/cesarnr21))
- chaee ([@chaee](https://github.com/chaee))
- lwileczek ([@lwileczek](https://github.com/lwileczek))
- magicpotato ([@fortheday](https://github.com/fortheday))
- motoacs ([@motoacs](https://github.com/motoacs))
- muzian666 ([@muzian666](https://github.com/muzian666))
@ -375,6 +399,23 @@ Support this project by becoming a sponsor. Your logo will show up in the Contri
<a href="https://opencollective.com/wikijs/sponsor/44/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/44/avatar.svg"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/40/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/45/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/41/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/46/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/42/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/47/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/43/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/48/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/44/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/49/avatar.svg"></a>
</td>
</tr>
</tbody>
</table>
</div>
@ -387,16 +428,21 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
<table><tbody><tr><td>
<img width="441" height="1" />
- Aeternum
- Al Romano
- Alex Balabanov
- Alex Milanov
- Alex Zen
- Arti Zirk
- Ave
- Brandon Curtis
- Damien Hottelier
- Daniel T. Holtzclaw
- Dave 'Sri' Seah
- djagoo
- dz
- Douglas Lassance
- Ergoflix
- Ernie Reid
- Etienne
- Flemis Jurgenheimer
@ -407,11 +453,11 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
- Ian
- Imari Childress
- Iskander Callos
- Josh Stewart
</td><td>
<img width="441" height="1" />
- Josh Stewart
- Justin Dunsworth
- Keir
- Loïc CRAMPON
@ -420,14 +466,17 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
- Mads Rosendahl
- Mark Mansur
- Matt Gedigian
- Mike Ditton
- Nate Figz
- Patryk
- Paul O'Fallon
- Philipp Schürch
- Tracey Duffy
- Quaxim
- Richeir
- Sergio Navarro Fernández
- Shad Narcher
- ShadowVoyd
- SmartNET.works
- Stepan Sokolovskyi
- Zach Crawford

@ -13,15 +13,10 @@ If you find such vulnerability, it's important to disclose it in a quick and sec
## Reporting a Vulnerability
**DO NOT CREATE AN ISSUE ON GITHUB** to report a potential vulnerability / security problem. Instead, choose one of these options:
> [!CAUTION]
> **DO NOT CREATE A GITHUB ISSUE / DISCUSSION** to report a potential vulnerability / security problem. Instead, use the process below:
### A) Disclose on Huntr.dev
Disclose the vulnerability on [Huntr.dev](https://huntr.dev/bounties/disclose) for the repository `https://github.com/Requarks/wiki`.
### B) Send an email
Send an email to security@requarks.io.
Submit a Vulnerability Report by filling in the form on https://github.com/requarks/wiki/security/advisories/new
Include as much details as possible, such as:
- The version(s) of Wiki.js that are impacted
@ -31,3 +26,6 @@ Include as much details as possible, such as:
- Your GitHub username if you'd like to be included as a collaborator on the private fix branch
The vulnerability will be investigated ASAP. If deemed valid, a draft security advisory will be created on GitHub and you will be included as a collaborator. A fix will be worked on in a private branch to resolves the issue. Once a fix is available, the advisory will be published.
> [!NOTE]
> There's no reward for submitting a report. As this is open source project and not corporate owned, we are not able to provide monetary rewards. You will however be credited as the bug reporter in the release notes.

@ -103,7 +103,7 @@ const graphQLLink = ApolloLink.from([
// Handle renewed JWT
const newJWT = resp.headers.get('new-jwt')
if (newJWT) {
Cookies.set('jwt', newJWT, { expires: 365 })
Cookies.set('jwt', newJWT, { expires: 365, secure: window.location.protocol === 'https:' })
}
return resp
}
@ -114,7 +114,11 @@ const graphQLWSLink = new WebSocketLink({
uri: graphQLWSEndpoint,
options: {
reconnect: true,
lazy: true
lazy: true,
connectionParams: () => {
const token = Cookies.get('jwt')
return token ? { token } : {}
}
}
})
@ -148,30 +152,30 @@ Vue.prototype.Velocity = Velocity
// Register Vue Components
// ====================================
Vue.component('admin', () => import(/* webpackChunkName: "admin" */ './components/admin.vue'))
Vue.component('comments', () => import(/* webpackChunkName: "comments" */ './components/comments.vue'))
Vue.component('editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue'))
Vue.component('history', () => import(/* webpackChunkName: "history" */ './components/history.vue'))
Vue.component('loader', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/loader.vue'))
Vue.component('login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue'))
Vue.component('nav-header', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue'))
Vue.component('new-page', () => import(/* webpackChunkName: "new-page" */ './components/new-page.vue'))
Vue.component('notify', () => import(/* webpackMode: "eager" */ './components/common/notify.vue'))
Vue.component('not-found', () => import(/* webpackChunkName: "not-found" */ './components/not-found.vue'))
Vue.component('page-selector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))
Vue.component('page-source', () => import(/* webpackChunkName: "source" */ './components/source.vue'))
Vue.component('profile', () => import(/* webpackChunkName: "profile" */ './components/profile.vue'))
Vue.component('register', () => import(/* webpackChunkName: "register" */ './components/register.vue'))
Vue.component('search-results', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/search-results.vue'))
Vue.component('social-sharing', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/social-sharing.vue'))
Vue.component('tags', () => import(/* webpackChunkName: "tags" */ './components/tags.vue'))
Vue.component('unauthorized', () => import(/* webpackChunkName: "unauthorized" */ './components/unauthorized.vue'))
Vue.component('v-card-chin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue'))
Vue.component('v-card-info', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-info.vue'))
Vue.component('welcome', () => import(/* webpackChunkName: "welcome" */ './components/welcome.vue'))
Vue.component('nav-footer', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/nav-footer.vue'))
Vue.component('page', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/page.vue'))
Vue.component('Admin', () => import(/* webpackChunkName: "admin" */ './components/admin.vue'))
Vue.component('Comments', () => import(/* webpackChunkName: "comments" */ './components/comments.vue'))
Vue.component('Editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue'))
Vue.component('History', () => import(/* webpackChunkName: "history" */ './components/history.vue'))
Vue.component('Loader', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/loader.vue'))
Vue.component('Login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue'))
Vue.component('NavHeader', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue'))
Vue.component('NewPage', () => import(/* webpackChunkName: "new-page" */ './components/new-page.vue'))
Vue.component('Notify', () => import(/* webpackMode: "eager" */ './components/common/notify.vue'))
Vue.component('NotFound', () => import(/* webpackChunkName: "not-found" */ './components/not-found.vue'))
Vue.component('PageSelector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))
Vue.component('PageSource', () => import(/* webpackChunkName: "source" */ './components/source.vue'))
Vue.component('Profile', () => import(/* webpackChunkName: "profile" */ './components/profile.vue'))
Vue.component('Register', () => import(/* webpackChunkName: "register" */ './components/register.vue'))
Vue.component('SearchResults', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/search-results.vue'))
Vue.component('SocialSharing', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/social-sharing.vue'))
Vue.component('Tags', () => import(/* webpackChunkName: "tags" */ './components/tags.vue'))
Vue.component('Unauthorized', () => import(/* webpackChunkName: "unauthorized" */ './components/unauthorized.vue'))
Vue.component('VCardChin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue'))
Vue.component('VCardInfo', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-info.vue'))
Vue.component('Welcome', () => import(/* webpackChunkName: "welcome" */ './components/welcome.vue'))
Vue.component('NavFooter', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/nav-footer.vue'))
Vue.component('Page', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/page.vue'))
let bootstrap = () => {
// ====================================

@ -82,6 +82,15 @@
:return-object='false'
:hint='$t(`admin:general.contentLicenseHint`)'
persistent-hint
)
v-text-field.mt-3(
outlined
:label='$t(`admin:general.footerOverride`)'
v-model='config.footerOverride'
prepend-icon='mdi-page-layout-footer'
append-icon='mdi-language-markdown'
persistent-hint
:hint='$t(`admin:general.footerOverrideHint`)'
)
v-divider
.overline.grey--text.pa-4 SEO
@ -280,6 +289,7 @@ export default {
analyticsId: '',
company: '',
contentLicense: '',
footerOverride: '',
logoUrl: '',
featureAnalytics: false,
featurePageRatings: false,
@ -308,6 +318,7 @@ export default {
logoUrl: sync('site/logoUrl'),
company: sync('site/company'),
contentLicense: sync('site/contentLicense'),
footerOverride: sync('site/footerOverride'),
activeModal: sync('editor/activeModal'),
contentLicenses () {
return [
@ -346,6 +357,7 @@ export default {
$analyticsId: String
$company: String
$contentLicense: String
$footerOverride: String
$logoUrl: String
$pageExtensions: String
$featurePageRatings: Boolean
@ -369,6 +381,7 @@ export default {
analyticsId: $analyticsId
company: $company
contentLicense: $contentLicense
footerOverride: $footerOverride
logoUrl: $logoUrl
pageExtensions: $pageExtensions
featurePageRatings: $featurePageRatings
@ -401,6 +414,7 @@ export default {
analyticsId: _.get(this.config, 'analyticsId', ''),
company: _.get(this.config, 'company', ''),
contentLicense: _.get(this.config, 'contentLicense', ''),
footerOverride: _.get(this.config, 'footerOverride', ''),
logoUrl: _.get(this.config, 'logoUrl', ''),
pageExtensions: _.get(this.config, 'pageExtensions', ''),
featurePageRatings: _.get(this.config, 'featurePageRatings', false),
@ -426,6 +440,7 @@ export default {
this.siteTitle = this.config.title
this.company = this.config.company
this.contentLicense = this.config.contentLicense
this.footerOverride = this.config.footerOverride
this.logoUrl = this.config.logoUrl
} catch (err) {
this.$store.commit('pushGraphError', err)
@ -461,6 +476,7 @@ export default {
analyticsId
company
contentLicense
footerOverride
logoUrl
pageExtensions
featurePageRatings

@ -149,28 +149,28 @@ export default {
items: [
{
permission: 'write:users',
hint: 'Can create or authorize new users, but not modify existing ones',
hint: 'Can create or authorize new users, but not modify existing ones. Can only assign to non-administrative groups',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:users',
hint: 'Can manage all users (but not users with administrative permissions)',
hint: 'Can create, authorize and modify ANY users. Can only assign to non-administrative groups',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'write:groups',
hint: 'Can manage groups and assign CONTENT permissions / page rules',
hint: 'Can manage groups and set CONTENT permissions / page rules. Can only assign users to non-administrative groups',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:groups',
hint: 'Can manage groups and assign ANY permissions (but not manage:system) / page rules',
hint: 'Can manage groups and set ANY permissions (but not manage:system) / page rules. Can assign users to ANY groups (except groups with the manage:system permission)',
warning: true,
restrictedForSystem: true,
disabled: false
@ -203,7 +203,7 @@ export default {
},
{
permission: 'manage:system',
hint: 'Can manage and access everything. Root administrator.',
hint: 'Can manage and access everything. Root administrator',
warning: true,
restrictedForSystem: true,
disabled: true

@ -39,14 +39,14 @@
v-list-item-icon
v-icon(color='indigo') mdi-pencil
v-list-item-title Edit
v-list-item(@click='', disabled)
v-list-item-icon
v-icon(color='grey') mdi-cube-scan
v-list-item-title Re-Render
v-list-item(@click='', disabled)
v-list-item-icon
v-icon(color='grey') mdi-earth-remove
v-list-item-title Unpublish
//- v-list-item(@click='', disabled)
//- v-list-item-icon
//- v-icon(color='grey') mdi-cube-scan
//- v-list-item-title Re-Render
//- v-list-item(@click='', disabled)
//- v-list-item-icon
//- v-icon(color='grey') mdi-earth-remove
//- v-list-item-title Unpublish
v-list-item(:href='`/s/` + page.locale + `/` + page.path')
v-list-item-icon
v-icon(color='indigo') mdi-code-tags
@ -55,14 +55,14 @@
v-list-item-icon
v-icon(color='indigo') mdi-history
v-list-item-title View History
v-list-item(@click='', disabled)
v-list-item-icon
v-icon(color='grey') mdi-content-duplicate
v-list-item-title Duplicate
v-list-item(@click='', disabled)
v-list-item-icon
v-icon(color='grey') mdi-content-save-move-outline
v-list-item-title Move / Rename
//- v-list-item(@click='', disabled)
//- v-list-item-icon
//- v-icon(color='grey') mdi-content-duplicate
//- v-list-item-title Duplicate
//- v-list-item(@click='', disabled)
//- v-list-item-icon
//- v-icon(color='grey') mdi-content-save-move-outline
//- v-list-item-title Move / Rename
v-dialog(v-model='deletePageDialog', max-width='500')
template(v-slot:activator='{ on }')
v-list-item(v-on='on')

@ -104,7 +104,7 @@ export default {
const truncatePath = path => _.take(path.split('/'), depth).join('/')
const descendantsByChild =
Object.entries(_.groupBy(descendants, page => truncatePath(page.path)))
.map(([childPath, descendantsGroup]) => [getPage(childPath), descendantsGroup])
.map(([childPath, descendantsGroup]) => [getPage(childPath), _.sortBy(descendantsGroup, child => child.path)])
.map(([child, descendantsGroup]) =>
[child, _.filter(descendantsGroup, d => d.path !== child.path)])
return {

@ -10,9 +10,9 @@
v-spacer
v-btn.animated.fadeInDown.wait-p1s(icon, color='grey', outlined, @click='refresh')
v-icon.grey--text mdi-refresh
v-btn.animated.fadeInDown.mx-3(color='primary', outlined, @click='recyclebin', disabled)
v-icon(left) mdi-delete-outline
span Recycle Bin
//- v-btn.animated.fadeInDown.mx-3(color='primary', outlined, @click='recyclebin', disabled)
//- v-icon(left) mdi-delete-outline
//- span Recycle Bin
v-btn.animated.fadeInDown(color='primary', depressed, large, to='pages/visualize')
v-icon(left) mdi-graph
span Visualize

@ -265,7 +265,7 @@ export default {
securityOpenRedirect: true,
securityIframe: true,
securityReferrerPolicy: true,
securityTrustProxy: true,
securityTrustProxy: false,
securitySRI: true,
securityHSTS: false,
securityHSTSDuration: 0,

@ -70,13 +70,13 @@
v-model='mustChangePwd'
hide-details
)
v-checkbox(
color='primary'
label='Send a welcome email'
hide-details
v-model='sendWelcomeEmail'
disabled
)
//- v-checkbox(
//- color='primary'
//- label='Send a welcome email'
//- hide-details
//- v-model='sendWelcomeEmail'
//- disabled
//- )
v-card-chin
v-spacer
v-btn(text, @click='isShown = false') Cancel

@ -337,12 +337,12 @@
.caption.grey--text.mt-3 {{$t('profile:activity.lastLoginOn')}}
.body-2: strong {{ user.lastLoginAt | moment('LLLL') }}
v-card.mt-3.animated.fadeInUp.wait-p6s
v-toolbar(color='teal', dense, dark, flat)
v-icon.mr-2 mdi-file-document-box-multiple-outline
span Content
v-card-text
em.caption.grey--text Coming soon
//- v-card.mt-3.animated.fadeInUp.wait-p6s
//- v-toolbar(color='teal', dense, dark, flat)
//- v-icon.mr-2 mdi-file-document-box-multiple-outline
//- span Content
//- v-card-text
//- em.caption.grey--text Coming soon
v-dialog(v-model='deleteUserDialog', max-width='500')
v-card

@ -15,7 +15,7 @@
prepend-inner-icon='mdi-magnify'
:loading='searchIsLoading'
@keyup.enter='searchEnter'
autocomplete='none'
autocomplete='off'
)
v-layout(row)
v-flex(xs5, md4)
@ -68,7 +68,7 @@
@blur='searchBlur'
@keyup.down='searchMove(`down`)'
@keyup.up='searchMove(`up`)'
autocomplete='none'
autocomplete='off'
)
v-tooltip(bottom)
template(v-slot:activator='{ on }')
@ -476,7 +476,11 @@ export default {
window.location.assign('/logout')
},
goHome () {
window.location.assign('/')
if (this.locales && this.locales.length > 0) {
window.location.assign(`/${this.locale}/home`)
} else {
window.location.assign('/')
}
}
}
}

@ -228,7 +228,8 @@ export default {
})
this.previewHTML = DOMPurify.sanitize($.html(), {
ADD_TAGS: ['foreignObject']
ADD_TAGS: ['foreignObject'],
HTML_INTEGRATION_POINTS: { foreignobject: true }
})
},
/**

@ -200,7 +200,7 @@ import 'codemirror/addon/fold/foldgutter.css'
import MarkdownIt from 'markdown-it'
import mdAttrs from 'markdown-it-attrs'
import mdDecorate from 'markdown-it-decorate'
import mdEmoji from 'markdown-it-emoji'
import { full as mdEmoji } from 'markdown-it-emoji'
import mdTaskLists from 'markdown-it-task-lists'
import mdExpandTabs from 'markdown-it-expand-tabs'
import mdAbbr from 'markdown-it-abbr'
@ -454,7 +454,8 @@ export default {
// this.$store.set('editor/content', newContent)
this.processMarkers(this.cm.firstLine(), this.cm.lastLine())
this.previewHTML = DOMPurify.sanitize(md.render(newContent), {
ADD_TAGS: ['foreignObject']
ADD_TAGS: ['foreignObject'],
HTML_INTEGRATION_POINTS: { foreignobject: true }
})
this.$nextTick(() => {
tabsetHelper.format()

@ -83,31 +83,31 @@
v-btn(icon, v-on='on', tile, small, @click.left='currentFileId = props.item.id')
v-icon(color='grey darken-2') mdi-dots-horizontal
v-list(nav, style='border-top: 5px solid #444;')
v-list-item(@click='', disabled)
v-list-item-avatar(size='24')
v-icon(color='teal') mdi-text-short
v-list-item-content {{$t('common:actions.properties')}}
template(v-if='props.item.kind === `IMAGE`')
v-list-item(@click='previewDialog = true', disabled)
v-list-item-avatar(size='24')
v-icon(color='green') mdi-image-search-outline
v-list-item-content {{$t('common:actions.preview')}}
v-list-item(@click='', disabled)
v-list-item-avatar(size='24')
v-icon(color='indigo') mdi-crop-rotate
v-list-item-content {{$t('common:actions.edit')}}
v-list-item(@click='', disabled)
v-list-item-avatar(size='24')
v-icon(color='purple') mdi-flash-circle
v-list-item-content {{$t('common:actions.optimize')}}
//- v-list-item(@click='', disabled)
//- v-list-item-avatar(size='24')
//- v-icon(color='teal') mdi-text-short
//- v-list-item-content {{$t('common:actions.properties')}}
//- template(v-if='props.item.kind === `IMAGE`')
//- v-list-item(@click='previewDialog = true', disabled)
//- v-list-item-avatar(size='24')
//- v-icon(color='green') mdi-image-search-outline
//- v-list-item-content {{$t('common:actions.preview')}}
//- v-list-item(@click='', disabled)
//- v-list-item-avatar(size='24')
//- v-icon(color='indigo') mdi-crop-rotate
//- v-list-item-content {{$t('common:actions.edit')}}
//- v-list-item(@click='', disabled)
//- v-list-item-avatar(size='24')
//- v-icon(color='purple') mdi-flash-circle
//- v-list-item-content {{$t('common:actions.optimize')}}
v-list-item(@click='openRenameDialog')
v-list-item-avatar(size='24')
v-icon(color='orange') mdi-keyboard-outline
v-list-item-content {{$t('common:actions.rename')}}
v-list-item(@click='', disabled)
v-list-item-avatar(size='24')
v-icon(color='blue') mdi-file-move
v-list-item-content {{$t('common:actions.move')}}
//- v-list-item(@click='', disabled)
//- v-list-item-avatar(size='24')
//- v-icon(color='blue') mdi-file-move
//- v-list-item-content {{$t('common:actions.move')}}
v-list-item(@click='deleteDialog = true')
v-list-item-avatar(size='24')
v-icon(color='red') mdi-file-hidden
@ -154,25 +154,25 @@
v-spacer
v-btn.px-4(color='teal', dark, @click='upload') {{$t('common:actions.upload')}}
v-card.mt-3.radius-7.animated.fadeInRight.wait-p4s(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')
v-card-text.pb-0
v-toolbar.radius-7(:color='$vuetify.theme.dark ? `teal` : `teal lighten-5`', dense, flat)
v-icon.mr-3(:color='$vuetify.theme.dark ? `white` : `teal`') mdi-cloud-download
.body-2(:class='$vuetify.theme.dark ? `white--text` : `teal--text`') {{$t('editor:assets.fetchImage')}}
v-spacer
v-chip(label, color='white', small).teal--text coming soon
v-text-field.mt-3(
v-model='remoteImageUrl'
outlined
color='teal'
single-line
placeholder='https://example.com/image.jpg'
)
v-divider
v-card-actions.pa-3
.caption.grey--text.text-darken-2 Max 5 MB
v-spacer
v-btn.px-4(color='teal', disabled) {{$t('common:actions.fetch')}}
//- v-card.mt-3.radius-7.animated.fadeInRight.wait-p4s(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')
//- v-card-text.pb-0
//- v-toolbar.radius-7(:color='$vuetify.theme.dark ? `teal` : `teal lighten-5`', dense, flat)
//- v-icon.mr-3(:color='$vuetify.theme.dark ? `white` : `teal`') mdi-cloud-download
//- .body-2(:class='$vuetify.theme.dark ? `white--text` : `teal--text`') {{$t('editor:assets.fetchImage')}}
//- v-spacer
//- v-chip(label, color='white', small).teal--text coming soon
//- v-text-field.mt-3(
//- v-model='remoteImageUrl'
//- outlined
//- color='teal'
//- single-line
//- placeholder='https://example.com/image.jpg'
//- )
//- v-divider
//- v-card-actions.pa-3
//- .caption.grey--text.text-darken-2 Max 5 MB
//- v-spacer
//- v-btn.px-4(color='teal', disabled) {{$t('common:actions.fetch')}}
v-card.mt-3.radius-7.animated.fadeInRight.wait-p4s(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')
v-card-text.pb-0

@ -21,7 +21,7 @@
v-tab {{$t('editor:props.info')}}
v-tab {{$t('editor:props.scheduling')}}
v-tab(:disabled='!hasScriptPermission') {{$t('editor:props.scripts')}}
v-tab(disabled) {{$t('editor:props.social')}}
//- v-tab(disabled) {{$t('editor:props.social')}}
v-tab(:disabled='!hasStylePermission') {{$t('editor:props.styles')}}
v-tab-item(transition='fade-transition', reverse-transition='fade-transition')
v-card-text.pt-5
@ -196,42 +196,42 @@
.editor-props-codeeditor-hint
.caption {{$t('editor:props.htmlHint')}}
v-tab-item(transition='fade-transition', reverse-transition='fade-transition')
v-card-text
.overline {{$t('editor:props.socialFeatures')}}
v-switch(
:label='$t(`editor:props.allowComments`)'
v-model='isPublished'
color='primary'
:hint='$t(`editor:props.allowCommentsHint`)'
persistent-hint
inset
)
v-switch(
:label='$t(`editor:props.allowRatings`)'
v-model='isPublished'
color='primary'
:hint='$t(`editor:props.allowRatingsHint`)'
persistent-hint
disabled
inset
)
v-switch(
:label='$t(`editor:props.displayAuthor`)'
v-model='isPublished'
color='primary'
:hint='$t(`editor:props.displayAuthorHint`)'
persistent-hint
inset
)
v-switch(
:label='$t(`editor:props.displaySharingBar`)'
v-model='isPublished'
color='primary'
:hint='$t(`editor:props.displaySharingBarHint`)'
persistent-hint
inset
)
//- v-tab-item(transition='fade-transition', reverse-transition='fade-transition')
//- v-card-text
//- .overline {{$t('editor:props.socialFeatures')}}
//- v-switch(
//- :label='$t(`editor:props.allowComments`)'
//- v-model='isPublished'
//- color='primary'
//- :hint='$t(`editor:props.allowCommentsHint`)'
//- persistent-hint
//- inset
//- )
//- v-switch(
//- :label='$t(`editor:props.allowRatings`)'
//- v-model='isPublished'
//- color='primary'
//- :hint='$t(`editor:props.allowRatingsHint`)'
//- persistent-hint
//- disabled
//- inset
//- )
//- v-switch(
//- :label='$t(`editor:props.displayAuthor`)'
//- v-model='isPublished'
//- color='primary'
//- :hint='$t(`editor:props.displayAuthorHint`)'
//- persistent-hint
//- inset
//- )
//- v-switch(
//- :label='$t(`editor:props.displaySharingBar`)'
//- v-model='isPublished'
//- color='primary'
//- :hint='$t(`editor:props.displaySharingBarHint`)'
//- persistent-hint
//- inset
//- )
v-tab-item(:transition='false', :reverse-transition='false')
.editor-props-codeeditor-title
@ -276,10 +276,10 @@ export default {
currentTab: 0,
cm: null,
rules: {
required: value => !!value || 'This field is required.',
path: value => {
return filenamePattern.test(value) || 'Invalid path. Please ensure it does not contain special characters, or begin/end in a slash or hashtag string.'
}
required: value => !!value || 'This field is required.',
path: value => {
return filenamePattern.test(value) || 'Invalid path. Please ensure it does not contain special characters, or begin/end in a slash or hashtag string.'
}
}
}
},
@ -334,7 +334,7 @@ export default {
this.loadEditor(this.$refs.codejs, 'html')
}, 100)
})
} else if (newValue === 4) {
} else if (newValue === 3) {
this.$nextTick(() => {
setTimeout(() => {
this.loadEditor(this.$refs.codecss, 'css')

@ -641,19 +641,25 @@ export default {
} else {
this.loaderColor = 'green darken-1'
this.loaderTitle = this.$t('auth:loginSuccess')
Cookies.set('jwt', respObj.jwt, { expires: 365 })
Cookies.set('jwt', respObj.jwt, { expires: 365, secure: window.location.protocol === 'https:' })
_.delay(() => {
const loginRedirect = Cookies.get('loginRedirect')
const isValidRedirect = loginRedirect && loginRedirect.startsWith('/') && !loginRedirect.startsWith('//') && !loginRedirect.includes('://')
if (loginRedirect === '/' && respObj.redirect) {
Cookies.remove('loginRedirect')
window.location.replace(respObj.redirect)
} else if (loginRedirect) {
} else if (isValidRedirect) {
Cookies.remove('loginRedirect')
window.location.replace(loginRedirect)
} else if (respObj.redirect) {
window.location.replace(respObj.redirect)
} else {
window.location.replace('/')
if (loginRedirect) {
Cookies.remove('loginRedirect')
}
if (respObj.redirect) {
window.location.replace(respObj.redirect)
} else {
window.location.replace('/')
}
}
}, 1000)
}

@ -129,41 +129,43 @@
//- v-btn(color='purple darken-4', disabled).ml-0 Enable 2FA
//- v-btn(color='purple darken-4', dark, depressed, disabled).ml-0 Disable 2FA
template(v-if='user.providerKey === `local`')
v-divider.mt-3
v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.changePassword')}}
v-text-field(
ref='iptCurrentPass'
v-model='currentPass'
outlined
:label='$t(`profile:auth.currentPassword`)'
type='password'
prepend-inner-icon='mdi-form-textbox-password'
)
v-text-field(
ref='iptNewPass'
v-model='newPass'
outlined
:label='$t(`profile:auth.newPassword`)'
type='password'
prepend-inner-icon='mdi-form-textbox-password'
autocomplete='off'
counter='255'
loading
)
password-strength(slot='progress', v-model='newPass')
v-text-field(
ref='iptVerifyPass'
v-model='verifyPass'
outlined
:label='$t(`profile:auth.verifyPassword`)'
type='password'
prepend-inner-icon='mdi-form-textbox-password'
autocomplete='off'
hide-details
)
form#change-password-form(@submit.prevent='changePassword')
v-divider.mt-3
v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.changePassword')}}
v-text-field(
ref='iptCurrentPass'
v-model='currentPass'
outlined
:label='$t(`profile:auth.currentPassword`)'
type='password'
prepend-inner-icon='mdi-form-textbox-password'
autocomplete='current-password'
)
v-text-field(
ref='iptNewPass'
v-model='newPass'
outlined
:label='$t(`profile:auth.newPassword`)'
type='password'
prepend-inner-icon='mdi-form-textbox-password'
autocomplete='off'
counter='255'
loading
)
password-strength(slot='progress', v-model='newPass')
v-text-field(
ref='iptVerifyPass'
v-model='verifyPass'
outlined
:label='$t(`profile:auth.verifyPassword`)'
type='password'
prepend-inner-icon='mdi-form-textbox-password'
autocomplete='off'
hide-details
)
v-card-chin(v-if='user.providerKey === `local`')
v-spacer
v-btn.px-4(color='purple darken-4', dark, depressed, @click='changePassword', :loading='changePassLoading')
v-btn.px-4(color='purple darken-4', dark, depressed, :loading='changePassLoading', type='submit', form='change-password-form')
v-icon(left) mdi-progress-check
span {{$t('profile:auth.changePassword')}}
v-flex(lg6 xs12)
@ -755,7 +757,7 @@ export default {
})
const resp = _.get(respRaw, 'data.users.updateProfile.responseResult', {})
if (resp.succeeded) {
Cookies.set('jwt', _.get(respRaw, 'data.users.updateProfile.jwt', ''), { expires: 365 })
Cookies.set('jwt', _.get(respRaw, 'data.users.updateProfile.jwt', ''), { expires: 365, secure: window.location.protocol === 'https:' })
this.$store.set('user/name', this.user.name)
this.$store.commit('showNotification', {
message: this.$t('profile:save.success'),
@ -863,7 +865,7 @@ export default {
this.currentPass = ''
this.newPass = ''
this.verifyPass = ''
Cookies.set('jwt', _.get(respRaw, 'data.users.changePassword.jwt', ''), { expires: 365 })
Cookies.set('jwt', _.get(respRaw, 'data.users.changePassword.jwt', ''), { expires: 365, secure: window.location.protocol === 'https:' })
this.$store.commit('showNotification', {
message: this.$t('profile:auth.changePassSuccess'),
style: 'success',

@ -20,8 +20,7 @@
v-card.grey.radius-7(flat, :class='$vuetify.theme.dark ? `darken-4` : `lighten-4`')
v-card-text
pre
code
slot
slot
nav-footer
notify

@ -98,6 +98,7 @@
:search='innerSearch'
:loading='isLoading'
:options.sync='pagination'
@page-count='pageTotal = $event'
hide-default-footer
ref='dude'
)
@ -183,6 +184,7 @@ export default {
sortDesc: [false]
},
pages: [],
pageTotal: 0,
isLoading: true,
scrollStyle: {
vuescroll: {},
@ -214,9 +216,6 @@ export default {
tagsSelected () {
return _.filter(this.tags, t => _.includes(this.selection, t.tag))
},
pageTotal () {
return Math.ceil(this.pages.length / this.pagination.itemsPerPage)
},
orderByItems () {
return [
{ text: this.$t('tags:orderByField.creationDate'), value: 'createdAt' },

@ -5,6 +5,7 @@ import { make } from 'vuex-pathify'
const state = {
company: siteConfig.company,
contentLicense: siteConfig.contentLicense,
footerOverride: siteConfig.footerOverride,
dark: siteConfig.darkMode,
tocPosition: siteConfig.tocPosition,
mascot: true,

@ -1,7 +1,9 @@
<template lang="pug">
v-footer.justify-center(:color='bgColor', inset)
.caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`')
template(v-if='company && company.length > 0 && contentLicense !== ``')
template(v-if='footerOverride')
span(v-html='footerOverrideRender + ` |&nbsp;`')
template(v-else-if='company && company.length > 0 && contentLicense !== ``')
span(v-if='contentLicense === `alr`') {{ $t('common:footer.copyright', { company: company, year: currentYear, interpolation: { escapeValue: false } }) }} |&nbsp;
span(v-else) {{ $t('common:footer.license', { company: company, license: $t('common:license.' + contentLicense), interpolation: { escapeValue: false } }) }} |&nbsp;
span {{ $t('common:footer.poweredBy') }} #[a(href='https://wiki.js.org', ref='nofollow') Wiki.js]
@ -9,6 +11,13 @@
<script>
import { get } from 'vuex-pathify'
import MarkdownIt from 'markdown-it'
const md = new MarkdownIt({
html: false,
breaks: false,
linkify: true
})
export default {
props: {
@ -29,6 +38,11 @@ export default {
computed: {
company: get('site/company'),
contentLicense: get('site/contentLicense'),
footerOverride: get('site/footerOverride'),
footerOverrideRender () {
if (!this.footerOverride) { return '' }
return md.renderInline(this.footerOverride)
},
bgColor() {
if (!this.$vuetify.theme.dark) {
return this.color

@ -377,6 +377,8 @@ import _ from 'lodash'
import ClipboardJS from 'clipboard'
import Vue from 'vue'
/* global siteLangs */
Vue.component('Tabset', Tabset)
Prism.plugins.autoloader.languages_path = '/_assets/js/prism/'
@ -503,6 +505,7 @@ export default {
},
data() {
return {
locales: siteLangs,
navShown: false,
navExpanded: false,
upBtnShown: false,
@ -549,13 +552,14 @@ export default {
}
},
breadcrumbs() {
return [{ path: '/', name: 'Home' }].concat(_.reduce(this.path.split('/'), (result, value, key) => {
result.push({
path: _.get(_.last(result), 'path', `/${this.locale}`) + `/${value}`,
name: value
})
return result
}, []))
return [{ path: '/', name: 'Home' }].concat(
_.reduce(this.path.split('/'), (result, value) => {
result.push({
path: _.get(_.last(result), 'path', this.locales.length > 0 ? `/${this.locale}` : '') + `/${value}`,
name: value
})
return result
}, []))
},
pageUrl () { return window.location.href },
upBtnPosition () {
@ -674,7 +678,11 @@ export default {
},
methods: {
goHome () {
window.location.assign('/')
if (this.locales && this.locales.length > 0) {
window.location.assign(`/${this.locale}/home`)
} else {
window.location.assign('/')
}
},
toggleNavigation () {
this.navOpen = !this.navOpen

@ -282,7 +282,7 @@
content: "\F02FC";
}
code {
code:not([class^="language-"]) {
background-color: mc('blue', '50');
color: mc('blue', '800');
}
@ -302,7 +302,7 @@
content: "\F0026";
}
code {
code:not([class^="language-"]) {
background-color: mc('orange', '50');
color: mc('orange', '800');
}
@ -323,7 +323,7 @@
content: "\F0159";
}
code {
code:not([class^="language-"]) {
background-color: mc('red', '50');
color: mc('red', '800');
}
@ -343,7 +343,7 @@
content: "\F0E1E";
}
code {
code:not([class^="language-"]) {
background-color: mc('green', '50');
color: mc('green', '800');
}
@ -685,6 +685,10 @@
display:inline-block;
vertical-align:top;
padding-top:0;
&:first-child {
width: 100%;
}
}
}
}
@ -760,8 +764,17 @@
.diagram {
margin-top: 1rem;
overflow: auto;
svg:first-child {
direction: ltr;
svg {
color-scheme: light !important;
&:first-child {
direction: ltr;
}
@at-root .theme--dark & {
color-scheme: dark !important;
}
}
}
@ -1271,6 +1284,8 @@
color: #000;
box-shadow: none;
text-shadow: none;
white-space: pre-wrap !important;
overflow-wrap: break-word !important;
}
}
}

@ -1,20 +1,22 @@
# =========================
# --- BUILD NPM MODULES ---
# =========================
FROM node:16-alpine AS build
FROM node:24-alpine AS build
RUN apk add yarn g++ make cmake python3 --no-cache
WORKDIR /wiki
COPY ./package.json ./package.json
COPY ./patches ./patches
RUN yarn --production --frozen-lockfile --non-interactive --network-timeout 100000
RUN yarn patch-package
# ===============
# --- Release ---
# ===============
FROM node:16-alpine
FROM node:24-alpine
LABEL maintainer="requarks.io"
RUN apk add bash curl git openssh gnupg sqlite --no-cache && \

@ -1,7 +1,7 @@
# ====================
# --- Build Assets ---
# ====================
FROM node:16-alpine AS assets
FROM node:24-alpine AS assets
RUN apk add yarn g++ make cmake python3 --no-cache
@ -9,6 +9,7 @@ WORKDIR /wiki
COPY ./client ./client
COPY ./dev ./dev
COPY ./patches ./patches
COPY ./package.json ./package.json
COPY ./.babelrc ./.babelrc
COPY ./.eslintignore ./.eslintignore
@ -19,11 +20,12 @@ RUN yarn --frozen-lockfile --non-interactive
RUN yarn build
RUN rm -rf /wiki/node_modules
RUN yarn --production --frozen-lockfile --non-interactive
RUN yarn patch-package
# ===============
# --- Release ---
# ===============
FROM node:16-alpine
FROM node:24-alpine
LABEL maintainer="requarks.io"
RUN apk add bash curl git openssh gnupg sqlite --no-cache && \
@ -51,4 +53,4 @@ EXPOSE 3443
# HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD curl -f http://localhost:3000/healthz
CMD ["node", "server"]
CMD ["node", "--no-deprecation", "server"]

@ -1,11 +1,11 @@
# -- DEV DOCKERFILE --
# -- DO NOT USE IN PRODUCTION! --
FROM node:14
FROM node:24
LABEL maintainer "requarks.io"
RUN apt-get update && \
apt-get install -y bash curl git python make g++ nano openssh-server gnupg && \
apt-get install -y bash curl git python3 make g++ nano openssh-server gnupg && \
mkdir -p /wiki
WORKDIR /wiki

@ -5,7 +5,7 @@ version: "3"
services:
db:
container_name: wiki-db
image: postgres:9-alpine
image: postgres:17-alpine
environment:
POSTGRES_DB: wiki
POSTGRES_PASSWORD: wikijsrocks

@ -2,7 +2,7 @@ version: "3"
services:
db:
image: postgres:11-alpine
image: postgres:15-alpine
environment:
POSTGRES_DB: wiki
POSTGRES_PASSWORD: wikijsrocks

@ -1,6 +0,0 @@
dependencies:
- name: postgresql
repository: https://charts.bitnami.com/bitnami
version: 8.10.14
digest: sha256:db7c1e0bc9ec0ed45520521bd76bb390d04711fd0f04affaadafa1dc498ce68b
generated: "2020-07-21T20:34:41.41180748-04:00"

@ -1,11 +1,7 @@
apiVersion: v2
name: wiki
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 2.2.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application.
AppVersion: latest
version: '3.0.0'
appVersion: '2'
description: The most powerful and extensible open source Wiki software.
keywords:
- wiki
@ -14,29 +10,8 @@ keywords:
- docs
- reference
- editor
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
dependencies:
- name: postgresql
version: 8.10.14
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
home: https://wiki.js.org
home: https://js.wiki
icon: https://cdn.js.wiki/images/wikijs-butterfly.svg
sources:
- https://github.com/Requarks/wiki
maintainers:
- name: Nicolas Giard
email: github@ngpixel.com
url: https://github.com/NGPixel
- name: James Greenhill
email: james@fuziontech.net
url: https://github.com/fuziontech
engine: gotpl
- https://github.com/requarks/wiki

@ -43,7 +43,7 @@ Wiki.js is an open source project that has been made possible due to the generou
This chart bootstraps a Wiki.js deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
It also optionally packages the [PostgreSQL](https://github.com/kubernetes/charts/tree/master/stable/postgresql) as the database but you are free to bring your own.
It also optionally deploys PostgreSQL as the database using the official PostgreSQL image from Docker Hub, but you are free to bring your own database.
## Prerequisites
@ -59,7 +59,7 @@ $ helm repo add requarks https://charts.js.wiki
To install the chart with the release name `my-release` run the following:
### Using Helm 3:
### Using Helm 3/4:
```console
$ helm install my-release requarks/wiki
```
@ -95,18 +95,18 @@ The following table lists the configurable parameters of the Wiki.js chart and t
| Parameter | Description | Default |
| ------------------------------- | ------------------------------- | ---------------------------------------------------------- |
| `image.repository` | Wiki.js image | `requarks/wiki` |
| `image.tag` | Wiki.js image tag | `latest` |
| `image.tag` | Wiki.js image tag | `2` |
| `imagePullPolicy` | Image pull policy | `IfNotPresent` |
| `replicacount` | Amount of wiki.js service pods to run | `1` |
| `revisionHistoryLimit` | Total amount of revision history points | `10` |
| `resources.limits` | wiki.js service resource limits | `nil` |
| `resources.requests` | wiki.js service resource requests | `nil` |
| `nodeSelector` | Node labels for wiki.js pod assignment | `{}` |
| `affinity` | Affinity settings for wiki.js pod assignment | `{}` |
| `schedulerName` | Name of an alternate scheduler for wiki.js pod | `nil` |
| `tolerations` | Toleration labels for wiki.jsk pod assignment | `[]` |
| `volumeMounts` | Volume mounts for Wiki.js container | `[]` |
| `volumes` | Volumes for Wiki.js Pod | `[]` |
| `replicacount` | Number of Wiki.js pods to run | `1` |
| `revisionHistoryLimit` | Total number of revision history points | `10` |
| `resources.limits` | Wiki.js service resource limits | `nil` |
| `resources.requests` | Wiki.js service resource requests | `nil` |
| `nodeSelector` | Node labels for the Wiki.js pod assignment | `{}` |
| `affinity` | Affinity settings for the Wiki.js pod assignment | `{}` |
| `schedulerName` | Name of an alternate scheduler for the Wiki.js pod | `nil` |
| `tolerations` | Toleration labels for the Wiki.js pod assignment | `[]` |
| `volumeMounts` | Volume mounts for the Wiki.js container | `[]` |
| `volumes` | Volumes for the Wiki.js pod | `[]` |
| `ingress.enabled` | Enable ingress controller resource | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | `{}` |
@ -114,21 +114,38 @@ The following table lists the configurable parameters of the Wiki.js chart and t
| `ingress.tls` | Ingress TLS configuration | `[]` |
| `sideload.enabled` | Enable sideloading of locale files from git | `false` |
| `sideload.repoURL` | Git repository URL containing locale files | `https://github.com/Requarks/wiki-localization` |
| `sideload.env` | Environment variables for sideload Container | `{}` |
| `sideload.env` | Environment variables for the sideload container | `{}` |
| `sideload.securityContext` | Security context for the sideload container | `nil` |
| `sideload.resources.limits` | Resource limits for the sideload container | `nil` |
| `sideload.resources.requests` | Resource requests for the sideload container | `nil` |
| `nodeExtraCaCerts` | Trusted certificates path | `nil` |
| `externalPostgresql.databaseURL` | External postgres connection string | `nil` |
| `postgresql.enabled` | Deploy postgres server (see below) | `true` |
| `postgresql.postgresqlDatabase` | Postgres database name | `wiki` |
| `postgresql.postgresqlUser` | Postgres username | `postgres` |
| `postgresql.postgresqlHost` | External postgres host | `nil` |
| `postgresql.postgresqlPassword` | External postgres password | `nil` |
| `postgresql.postgresqlHost` | Postgres host | `nil` |
| `postgresql.postgresqlPassword` | Postgres password | `nil` |
| `postgresql.existingSecret` | Provide an existing `Secret` for postgres | `nil` |
| `postgresql.existingSecretKey` | The postgres password key in the existing `Secret` | `postgresql-password` |
| `postgresql.postgresqlPort` | External postgres port | `5432` |
| `postgresql.existingSecretKey` | The postgres password key in the existing `Secret` | `postgresql-password` |
| `postgresql.existingSecretUserKey` | The postgres username key in the existing `Secret` | `postgresql-username` |
| `postgresql.postgresqlPort` | Postgres port | `5432` |
| `postgresql.ssl` | Enable external postgres SSL connection | `false` |
| `postgresql.ca` | Certificate of Authority content for postgres | `nil` |
| `postgresql.persistence.enabled` | Enable postgres persistence using PVC | `true` |
| `postgresql.persistence.existingClaim` | Provide an existing `PersistentVolumeClaim` for postgres | `nil` |
| `postgresql.persistence.storageClass` | Postgres PVC Storage Class (example: `nfs`) | `nil` |
| `postgresql.persistence.size` | Postgers PVC Storage Request | `8Gi` |
| `postgresql.persistence.size` | Postgres PVC Storage Request | `8Gi` |
| `postgresql.persistence.accessMode` | Postgres Persistent Volume Access Mode | `ReadWriteOnce` |
| `postgresql.image.repository` | PostgreSQL image repository | `postgres` |
| `postgresql.image.tag` | PostgreSQL image tag | `18` |
| `postgresql.image.pullPolicy` | PostgreSQL image pull policy | `IfNotPresent` |
| `postgresql.resources` | PostgreSQL resource requests/limits | `{}` |
| `postgresql.nodeSelector` | PostgreSQL node selector labels | `{}` |
| `postgresql.tolerations` | PostgreSQL toleration labels | `[]` |
| `postgresql.affinity` | PostgreSQL affinity settings | `{}` |
| `postgresql.service.type` | PostgreSQL service type | `ClusterIP` |
| `postgresql.service.port` | PostgreSQL service port | `5432` |
| `postgresql.service.annotations` | PostgreSQL service annotations | `{}` |
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,
@ -146,25 +163,44 @@ $ helm install --name my-release -f values.yaml requarks/wiki
> **Tip**: You can use the default [values.yaml](values.yaml)
## PostgresSQL
## PostgreSQL
By default, PostgreSQL is installed as part of the chart.
By default, PostgreSQL is installed as part of the chart using the official PostgreSQL image from Docker Hub (version 18).
### Using an external PostgreSQL server
To use an external PostgreSQL server, set `postgresql.enabled` to `false` and then set `postgresql.postgresqlHost` and `postgresql.postgresqlPassword`. To use an existing `Secret`, set `postgresql.existingSecret`. The other options (`postgresql.postgresqlDatabase`, `postgresql.postgresqlUser`, `postgresql.postgresqlPort` and `postgresql.existingSecretKey`) may also want changing from their default values.
To use an external PostgreSQL server, set `postgresql.enabled` to `false`, then use either:
To use an SSL connection you can set `postgresql.ssl` to `true` and if needed the path to a Certificate of Authority can be set using `postgresql.ca` to `/path/to/ca`. Default `postgresql.ssl` value is `false`.
#### Connection String
If `postgresql.existingSecret` is not specified, you also need to add the following Helm template to your deployment in order to create the postgresql `Secret`:
Set `externalPostgresql.databaseURL` to the full PostgreSQL connection string.
```yaml
kind: Secret
apiVersion: v1
metadata:
name: {{ template "wiki.postgresql.secret" . }}
data:
{{ template "wiki.postgresql.secretKey" . }}: "{{ .Values.postgresql.postgresqlPassword | b64enc }}"
#### Connection Parameters
Set `externalPostgresql.host`, `externalPostgres.port`, `externalPostgres.database`, `externalPostgres.username`, `externalPostgres.existingSecret` *(secret name)* and `externalPostgres.existingSecretKey` *(key in the secret containing the password)*
Ensure the secret specified in `externalPostgresql.existingSecret` already exists, with a password set at the path specified in `externalPostgres.existingSecretKey`.
To use an SSL connection you can set `externalPostgresql.ssl` to `true` and if needed the path to a Certificate of Authority can be set using `externalPostgresql.ca` to `/path/to/ca`. Default `externalPostgresql.ssl` value is `false`.
### Using an existing PostgreSQL secret with built-in PostgreSQL
When using the built-in PostgreSQL (default behavior with `postgresql.enabled: true`), you can still use an existing Kubernetes secret for the database credentials by setting:
- `postgresql.existingSecret`: Name of the existing secret containing the credentials
- `postgresql.existingSecretKey`: Key in the secret containing the password (defaults to `postgresql-password`)
- `postgresql.existingSecretUserKey`: Key in the secret containing the username (defaults to `postgresql-username`)
Example usage:
```bash
# Create your existing secret
kubectl create secret generic my-postgres-secret \
--from-literal=postgresql-username=postgres \
--from-literal=postgresql-password=yourpassword
# Deploy with existing secret
helm install my-release requarks/wiki \
--set postgresql.enabled=true \
--set postgresql.existingSecret=my-postgres-secret
```
## Persistence
@ -175,3 +211,38 @@ See the [Configuration](#configuration) section to configure the PVC or to disab
## Ingress
This chart provides support for Ingress resource. If you have an available Ingress Controller such as Nginx or Traefik you maybe want to set `ingress.enabled` to true and add `ingress.hosts` for the URL. Then, you should be able to access the installation using that address.
## Extra Trusted Certificates
To append extra CA Certificates:
1. Create a ConfigMap with CAs in PEM format, e.g.:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ca
namespace: your-wikijs-namespace
data:
certs.pem: |-
-----BEGIN CERTIFICATE-----
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-----END CERTIFICATE-----
```
2. Mount your CAs from the ConfigMap to the Wiki.js pod and set `nodeExtraCaCerts` helm variable. Insert the following lines to your Wiki.js `values.yaml`, e.g.:
```yaml
volumeMounts:
- name: ca
mountPath: /cas.pem
subPath: certs.pem
volumes:
- name: ca
configMap:
name: ca
nodeExtraCaCerts: "/cas.pem"
```

@ -19,3 +19,16 @@
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80
{{- end }}
{{- if .Values.postgresql.enabled }}
2. PostgreSQL database has been deployed as part of this release:
- Database: {{ .Values.postgresql.postgresqlDatabase }}
- User: {{ .Values.postgresql.postgresqlUser }}
- Service: {{ include "wiki.postgresql.fullname" . }}
- Version: {{ .Values.postgresql.image.tag }}
- Persistence: {{ .Values.postgresql.persistence.enabled | ternary "Enabled" "Disabled" }}
{{- end }}
{{- if not .Values.postgresql.enabled }}
2. External PostgreSQL setup detected. Ensure your database is accessible at the configured host.
{{- end }}

@ -63,15 +63,18 @@ Create the name of the service account to use
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
PostgreSQL fullname
*/}}
{{- define "wiki.postgresql.fullname" -}}
{{- if .Values.postgresql.fullnameOverride -}}
{{- .Values.postgresql.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{ printf "%s-%s" .Release.Name "postgresql"}}
{{- printf "%s-%s" (include "wiki.fullname" .) "postgresql" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
PostgreSQL selector labels
*/}}
{{- define "wiki.postgresql.selectorLabels" -}}
app.kubernetes.io/name: {{ include "wiki.name" . }}-postgresql
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
@ -79,9 +82,9 @@ Set postgres host
*/}}
{{- define "wiki.postgresql.host" -}}
{{- if .Values.postgresql.enabled -}}
{{- template "wiki.postgresql.fullname" . -}}
{{- include "wiki.postgresql.fullname" . -}}
{{- else -}}
{{- .Values.postgresql.postgresqlHost | quote -}}
{{- .Values.postgresql.postgresqlHost | default "localhost" | quote -}}
{{- end -}}
{{- end -}}
@ -89,10 +92,25 @@ Set postgres host
Set postgres secret
*/}}
{{- define "wiki.postgresql.secret" -}}
{{- if .Values.postgresql.enabled -}}
{{- template "wiki.postgresql.fullname" . -}}
{{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}}
{{- .Values.postgresql.existingSecret -}}
{{- else if .Values.postgresql.enabled -}}
{{- include "wiki.postgresql.fullname" . -}}
{{- else -}}
{{- template "wiki.fullname" . -}}
{{- template "wiki.fullname" . -}}
{{- end -}}
{{- end -}}
{{/*
Set postgres secretUserKey
*/}}
{{- define "wiki.postgresql.secretUserKey" -}}
{{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}}
{{- default "postgresql-username" .Values.postgresql.existingSecretUserKey | quote -}}
{{- else if .Values.postgresql.enabled -}}
"postgresql-username"
{{- else -}}
{{- default "postgresql-username" .Values.postgresql.existingSecretUserKey | quote -}}
{{- end -}}
{{- end -}}
@ -100,9 +118,24 @@ Set postgres secret
Set postgres secretKey
*/}}
{{- define "wiki.postgresql.secretKey" -}}
{{- if .Values.postgresql.enabled -}}
"postgresql-password"
{{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}}
{{- default "postgresql-password" .Values.postgresql.existingSecretKey | quote -}}
{{- else if .Values.postgresql.enabled -}}
"postgresql-password"
{{- else -}}
{{- default "postgresql-password" .Values.postgresql.existingSecretKey | quote -}}
{{- end -}}
{{- end -}}
{{/*
Set postgres secretDatabaseKey
*/}}
{{- define "wiki.postgresql.secretDatabaseKey" -}}
{{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}}
{{- default "postgresql-database" .Values.postgresql.existingSecretDatabaseKey | quote -}}
{{- else if .Values.postgresql.enabled -}}
"postgresql-database"
{{- else -}}
{{- default "postgresql-password" .Values.postgresql.existingSecretKey | quote -}}
{{- default "postgresql-database" .Values.postgresql.existingSecretDatabaseKey | quote -}}
{{- end -}}
{{- end -}}

@ -14,6 +14,8 @@ spec:
metadata:
labels:
{{- include "wiki.selectorLabels" . | nindent 8 }}
annotations:
{{- toYaml .Values.podAnnotations | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
@ -25,36 +27,51 @@ spec:
{{- if .Values.sideload.enabled }}
initContainers:
- name: {{ .Chart.Name }}-sideload
image: "{{ .Values.image.repository }}:{{ default "latest" .Values.image.tag }}"
securityContext:
{{- toYaml .Values.sideload.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ default "2" .Values.image.tag }}"
imagePullPolicy: {{ default "IfNotPresent" .Values.image.imagePullPolicy }}
env:
{{- toYaml .Values.sideload.env | nindent 12 }}
command: [ "sh", "-c" ]
args: [ "mkdir -p /wiki/data/sideload && git clone --depth=1 {{ .Values.sideload.repoURL }} /wiki/data/sideload/" ]
resources:
{{- toYaml .Values.sideload.resources | nindent 12 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ default "latest" .Values.image.tag }}"
image: "{{ .Values.image.repository }}:{{ default "2" .Values.image.tag }}"
imagePullPolicy: {{ default "IfNotPresent" .Values.image.imagePullPolicy }}
env:
{{- if .Values.nodeExtraCaCerts }}
- name: NODE_EXTRA_CA_CERTS
value: {{ .Values.nodeExtraCaCerts }}
{{- end }}
- name: DB_TYPE
value: postgres
{{- if (.Values.externalPostgresql).databaseURL }}
{{- if and .Values.externalPostgresql .Values.externalPostgresql.databaseURL }}
- name: DATABASE_URL
value: {{ .Values.externalPostgresql.databaseURL }}
- name: NODE_TLS_REJECT_UNAUTHORIZED
value: {{ default "1" .Values.externalPostgresql.NODE_TLS_REJECT_UNAUTHORIZED | quote }}
{{- else }}
{{- else if .Values.postgresql.enabled }}
- name: DB_HOST
value: {{ template "wiki.postgresql.host" . }}
- name: DB_PORT
value: "{{ default "5432" .Values.postgresql.postgresqlPort }}"
- name: DB_NAME
value: {{ default "wiki" .Values.postgresql.postgresqlDatabase }}
value: {{ default "wiki" .Values.postgresql.postgresqlDatabase | quote }}
- name: DB_USER
value: {{ default "wiki" .Values.postgresql.postgresqlUser }}
{{- if .Values.postgresql.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.existingSecret }}
key: {{ template "wiki.postgresql.secretUserKey" . }}
{{- else }}
value: {{ default "postgres" .Values.postgresql.postgresqlUser }}
{{- end }}
- name: DB_SSL
value: "{{ default "false" .Values.postgresql.ssl }}"
- name: DB_SSL_CA
@ -62,15 +79,33 @@ spec:
- name: DB_PASS
valueFrom:
secretKeyRef:
{{- if .Values.postgresql.existingSecret }}
name: {{ .Values.postgresql.existingSecret }}
{{- else }}
name: {{ template "wiki.postgresql.secret" . }}
{{- end }}
key: {{ template "wiki.postgresql.secretKey" . }}
{{- else if .Values.externalPostgresql }}
# External PostgreSQL configuration
- name: DB_HOST
value: {{ required "External PostgreSQL host is required when postgresql.enabled is false" .Values.externalPostgresql.host | quote }}
- name: DB_PORT
value: {{ required "External PostgreSQL port is required when postgresql.enabled is false" .Values.externalPostgresql.port | quote }}
- name: DB_NAME
value: {{ required "External PostgreSQL database name is required when postgresql.enabled is false" .Values.externalPostgresql.database | quote }}
- name: DB_USER
value: {{ required "External PostgreSQL user is required when postgresql.enabled is false" .Values.externalPostgresql.username | quote }}
- name: DB_PASS
valueFrom:
secretKeyRef:
name: {{ required "External PostgreSQL secret name is required when postgresql.enabled is false" .Values.externalPostgresql.existingSecret | quote }}
key: {{ required "External PostgreSQL secret key is required when postgresql.enabled is false" .Values.externalPostgresql.existingSecretKey | quote }}
- name: DB_SSL
value: "{{ default "false" .Values.externalPostgresql.ssl }}"
- name: DB_SSL_CA
value: "{{ default "" .Values.externalPostgresql.ca }}"
{{- end }}
- name: HA_ACTIVE
value: {{ .Values.replicaCount | int | le 2 | quote }}
{{- with .Values.extraEnvVars }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}

@ -0,0 +1,21 @@
{{- if and .Values.postgresql.enabled .Values.postgresql.persistence.enabled -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "wiki.postgresql.fullname" . }}
labels:
{{- include "wiki.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.postgresql.persistence.accessMode | quote }}
resources:
requests:
storage: {{ .Values.postgresql.persistence.size | quote }}
{{- if .Values.postgresql.persistence.storageClass }}
{{- if (eq "-" .Values.postgresql.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.postgresql.persistence.storageClass | quote }}
{{- end }}
{{- end }}
{{- end }}

@ -0,0 +1,12 @@
{{- if and .Values.postgresql.enabled (not .Values.postgresql.existingSecret) -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "wiki.postgresql.fullname" . }}
labels:
{{- include "wiki.labels" . | nindent 4 }}
type: Opaque
data:
postgresql-password: {{ .Values.postgresql.postgresqlPassword | b64enc | quote }}
postgresql-username: {{ .Values.postgresql.postgresqlUser | b64enc | quote }}
{{- end }}

@ -0,0 +1,21 @@
{{- if .Values.postgresql.enabled -}}
apiVersion: v1
kind: Service
metadata:
name: {{ include "wiki.postgresql.fullname" . }}
labels:
{{- include "wiki.labels" . | nindent 4 }}
{{- with .Values.postgresql.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.postgresql.service.type }}
ports:
- port: {{ .Values.postgresql.service.port }}
targetPort: 5432
protocol: TCP
name: postgresql
selector:
{{- include "wiki.postgresql.selectorLabels" . | nindent 4 }}
{{- end }}

@ -0,0 +1,101 @@
{{- if .Values.postgresql.enabled -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "wiki.postgresql.fullname" . }}
labels:
{{- include "wiki.labels" . | nindent 4 }}
spec:
serviceName: {{ include "wiki.postgresql.fullname" . }}
replicas: 1
selector:
matchLabels:
{{- include "wiki.postgresql.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "wiki.postgresql.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.postgresql.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.postgresql.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.postgresql.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: postgresql
image: {{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}
imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }}
ports:
- containerPort: 5432
name: postgresql
env:
- name: POSTGRES_DB
value: {{ .Values.postgresql.postgresqlDatabase | quote }}
- name: POSTGRES_USER
{{- if .Values.postgresql.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.existingSecret }}
key: {{ default "postgresql-username" .Values.postgresql.existingSecretUserKey | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ include "wiki.postgresql.fullname" . }}
key: postgresql-username
{{- end }}
- name: POSTGRES_PASSWORD
{{- if .Values.postgresql.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.existingSecret }}
key: {{ default "postgresql-password" .Values.postgresql.existingSecretKey | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ include "wiki.postgresql.fullname" . }}
key: postgresql-password
{{- end }}
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
livenessProbe:
exec:
command:
- sh
- -c
- exec pg_isready -U {{ .Values.postgresql.postgresqlUser }} -d {{ .Values.postgresql.postgresqlDatabase }}
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
readinessProbe:
exec:
command:
- sh
- -c
- exec pg_isready -U {{ .Values.postgresql.postgresqlUser }} -d {{ .Values.postgresql.postgresqlDatabase }}
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 6
resources:
{{- toYaml .Values.postgresql.resources | nindent 12 }}
volumeMounts:
- name: postgresql-data
mountPath: /var/lib/postgresql/data
subPath: postgresql
volumes:
- name: postgresql-data
{{- if .Values.postgresql.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "wiki.postgresql.fullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}

@ -19,8 +19,4 @@ spec:
targetPort: http
protocol: TCP
name: http
- port: {{ default "443" .Values.service.httpsPort}}
targetPort: http
protocol: TCP
name: https
selector: {{- include "wiki.selectorLabels" . | nindent 4}}

@ -3,7 +3,7 @@
# Declare variables to be passed into your templates.
replicaCount: 1
revisionHistoryLimit: 10
revisionHistoryLimit: 2
image:
repository: requarks/wiki
@ -42,6 +42,8 @@ startupProbe:
path: /healthz
port: http
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
@ -59,7 +61,6 @@ service:
# Annotations applied for services such as externalDNS or
# service type LoadBalancer
# type: LoadBalancer
# httpsPort: 443
# annotations: {}
# loadBalancerIP: 172.16.0.1
@ -102,17 +103,51 @@ volumeMounts: []
volumes: []
# This will allow us to install locales even without internet access using a initContainer & wikjs "sideloading"
# This will allow us to install locales even without internet access using a initContainer & Wiki.js "sideloading"
sideload:
enabled: false
# Git-Repo containing all locales.json-files you need:
repoURL: https://github.com/Requarks/wiki-localization
repoURL: https://github.com/requarks/wiki-localization
## This can be helpfull if you have internet access over a http proxy:
env: []
# - name: HTTPS_PROXY
# value: http://my.proxy.com:3128
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
## Append extra trusted certificates for node process from extra volume via NODE_EXTRA_CA_CERTS variable
# nodeExtraCaCerts: "/path/to/certs.pem"
## Additional environment variables to set
extraEnvVars: []
# extraEnvVars:
# - name: CUSTOM_VAR
# value: "custom_value"
# - name: SECRET_VAR
# valueFrom:
# secretKeyRef:
# name: my-secret
# key: secret-key
## This will override the postgresql chart values
# externalPostgresql:
# # note: ?sslmode=require => ?ssl=true
@ -120,47 +155,52 @@ sideload:
# # For self signed CAs, like DigitalOcean
# NODE_TLS_REJECT_UNAUTHORIZED: "0"
## Configuration values for the postgresql dependency.
## ref: https://github.com/kubernetes/charts/blob/master/stable/postgresql/README.md
## Configuration for the custom PostgreSQL 18 deployment
##
postgresql:
## Use the PostgreSQL chart dependency.
## Set to false if bringing your own PostgreSQL, and set secret value postgresql-uri.
##
enabled: true
## ssl enforce SSL communication with PostgresSQL
## Default to false
##
# ssl: false
ssl: false
## ca Certificate of Authority
## Default to empty, point to location of CA
##
# ca: "path to ca"
## postgresqlHost override postgres database host
## Default to postgres
## Default to the service name of the custom PostgreSQL deployment
##
# postgresqlHost: postgres
postgresqlHost: "{{ include \"wiki.postgresql.fullname\" . }}"
## postgresqlPort port for postgres
## Default to 5432
##
# postgresqlPort: 5432
## PostgreSQL fullname Override
## Default to wiki-postgresql unless fullname override is set for Chart
##
fullnameOverride: ""
postgresqlPort: 5432
## PostgreSQL User to create.
##
postgresqlUser: postgres
## PostgreSQL Database to create.
##
postgresqlDatabase: wiki
## PostgreSQL password (will be stored in a secret)
##
postgresqlPassword: "postgres"
## Use existing secret for PostgreSQL credentials
## If set, the chart will not create a new secret and will use the existing one
##
# existingSecret: "my-existing-postgres-secret"
## Key in the existing secret containing the password
##
# existingSecretKey: "postgresql-password"
## Key in the existing secret containing the username (defaults to "postgresql-username")
##
# existingSecretUserKey: "postgresql-username"
## Persistent Volume Storage configuration.
## ref: https://kubernetes.io/docs/user-guide/persistent-volumes
##
replication:
## Enable PostgreSQL replication (primary/secondary)
##
enabled: false
persistence:
## Enable PostgreSQL persistence using Persistent Volume Claims.
##
@ -179,3 +219,34 @@ postgresql:
## Persistent Volume Storage Size.
##
size: 8Gi
## PostgreSQL Image Configuration
image:
repository: postgres
tag: "18"
pullPolicy: IfNotPresent
## PostgreSQL Resources Configuration
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
## PostgreSQL Node Selector, Tolerations and Affinity
nodeSelector: {}
tolerations: []
affinity: {}
## PostgreSQL Service Configuration
service:
type: ClusterIP
port: 5432
# Additional service annotations
annotations: {}

@ -2,10 +2,9 @@
"variables": {
"do_api_token": "{{env `DIGITALOCEAN_API_TOKEN`}}",
"image_name": "wikijs-snapshot-{{timestamp}}",
"apt_packages": "apt-transport-https ca-certificates curl jq linux-image-extra-virtual software-properties-common gnupg-agent openssl ",
"apt_packages": "software-properties-common",
"application_name": "Wiki.js",
"application_version": "{{env `WIKI_APP_VERSION`}}",
"docker_compose_version": "1.29.2"
"application_version": "{{env `WIKI_APP_VERSION`}}"
},
"sensitive-variables": [
"do_api_token"
@ -14,7 +13,7 @@
{
"type": "digitalocean",
"api_token": "{{user `do_api_token`}}",
"image": "ubuntu-20-04-x64",
"image": "ubuntu-24-04-x64",
"region": "tor1",
"size": "s-1vcpu-1gb",
"ssh_username": "root",
@ -73,11 +72,8 @@
],
"scripts": [
"scripts/010-docker.sh",
"scripts/011-docker-compose.sh",
"scripts/012-grub-opts.sh",
"scripts/013-docker-dns.sh",
"scripts/014-ufw-docker.sh",
"scripts/020-application-tag.sh",
"scripts/011-ufw-docker.sh",
"scripts/020-force-ssh-logout.sh",
"scripts/900-cleanup.sh",
"scripts/999-img-check.sh"
]

@ -1,15 +1,18 @@
#!/bin/bash
# Scripts in this directory will be executed by cloud-init on the first boot of droplets
# created from your image. Things ike generating passwords, configuration requiring IP address
# or other items that will be unique to each instance should be done in scripts here.
# Generate PostgreSQL password
openssl rand -base64 32 > /etc/wiki/.db-secret
# Start containers
if [[ -z $DATABASE_URL ]]; then
docker start db
fi
docker start wiki
docker start wiki-update-companion
# docker start nginx-proxy
# docker start watchtower
# Remove the ssh force logout command
sed -e '/Match User root/d' \
-e '/.*ForceCommand.*droplet.*/d' \
-i /etc/ssh/sshd_config
systemctl restart ssh

@ -1,17 +1,33 @@
#!/bin/bash
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt -qqy update
apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install docker-ce docker-ce-cli containerd.io
# Add Docker's official GPG key:
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt -qqy update
# Install Docker
sudo apt -qqy install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable docker
systemctl start docker
# Setup containers
mkdir -p /etc/wiki
docker network create wikinet
docker volume create pgdata
docker create --name=db -e POSTGRES_DB=wiki -e POSTGRES_USER=wiki -e POSTGRES_PASSWORD_FILE=/etc/wiki/.db-secret -v /etc/wiki/.db-secret:/etc/wiki/.db-secret:ro -v pgdata:/var/lib/postgresql/data --restart=unless-stopped -h db --network=wikinet postgres:11
docker create --name=db -e POSTGRES_DB=wiki -e POSTGRES_USER=wiki -e POSTGRES_PASSWORD_FILE=/etc/wiki/.db-secret -v /etc/wiki/.db-secret:/etc/wiki/.db-secret:ro -v pgdata:/var/lib/postgresql/data --restart=unless-stopped -h db --network=wikinet postgres:17
docker create --name=wiki -e DB_TYPE=postgres -e DB_HOST=db -e DB_PORT=5432 -e DB_PASS_FILE=/etc/wiki/.db-secret -v /etc/wiki/.db-secret:/etc/wiki/.db-secret:ro -e DB_USER=wiki -e DB_NAME=wiki -e UPGRADE_COMPANION=1 --restart=unless-stopped -h wiki --network=wikinet -p 80:3000 -p 443:3443 ghcr.io/requarks/wiki:2
docker create --name=wiki-update-companion -v /var/run/docker.sock:/var/run/docker.sock:ro --restart=unless-stopped -h wiki-update-companion --network=wikinet ghcr.io/requarks/wiki-update-companion:latest

@ -1,4 +0,0 @@
#!/bin/sh
sudo curl -L "https://github.com/docker/compose/releases/download/${docker_compose_version}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose;
chmod +x /usr/local/bin/docker-compose;

@ -0,0 +1,9 @@
#!/bin/bash
ufw limit ssh
ufw allow http
ufw allow https
ufw --force enable
cat /dev/null > /var/log/ufw.log

@ -1,6 +0,0 @@
#!/bin/sh
sed -e 's|GRUB_CMDLINE_LINUX="|GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1|g' \
-i /etc/default/grub
update-grub

@ -1,4 +0,0 @@
#!/bin/sh
sed -e 's|#DOCKER_OPTS|DOCKER_OPTS|g' \
-i /etc/default/docker

@ -1,9 +0,0 @@
#!/bin/bash
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw --force enable
cat /dev/null > /var/log/ufw.log

@ -1,24 +0,0 @@
#!/bin/sh
################################
## PART: Write the application tag
##
## vi: syntax=sh expandtab ts=4
build_date=$(date +%Y-%m-%d)
distro="$(lsb_release -s -i)"
distro_release="$(lsb_release -s -r)"
distro_codename="$(lsb_release -s -c)"
distro_arch="$(uname -m)"
mkdir -p /var/lib/digitalocean
cat >> /var/lib/digitalocean/application.info <<EOM
application_name="${application_name}"
build_date="${build_date}"
distro="${distro}"
distro_release="${distro_release}"
distro_codename="${distro_codename}"
distro_arch="${distro_arch}"
application_version="${application_version}"
EOM

@ -0,0 +1,6 @@
#!/bin/sh
cat >> /etc/ssh/sshd_config <<EOM
Match User root
ForceCommand echo "Please wait while we get your droplet ready..."
EOM

@ -5,17 +5,20 @@
myip=$(hostname -I | awk '{print$1}')
cat <<EOF
********************************************************************************
Welcome to Wiki.js's 1-Click DigitalOcean Droplet.
To keep this Droplet secure, the UFW firewall is enabled.
All ports are BLOCKED except 22 (SSH), 80 (Docker) and 443 (Docker).
* The Wiki.js 1-Click DigitalOcean Quickstart guide is available at:
https://docs.requarks.io/install/digitalocean
* You can SSH to this Droplet in a terminal as root: ssh root@$myip
* Docker is installed and configured per Docker's recommendations:
https://docs.docker.com/install/linux/docker-ce/ubuntu/
* Docker Compose is installed and configured per Docker's recommendations:
https://docs.docker.com/compose/install/#install-compose
https://docs.docker.com/engine/install/ubuntu/
For more information, visit https://docs.requarks.io/install/digitalocean
********************************************************************************
To delete this message of the day: rm -rf $(readlink -f ${0})
EOF

@ -1,5 +1,11 @@
#!/bin/bash
# DigitalOcean Marketplace Image Validation Tool
# © 2021 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
set -o errexit
# Ensure /tmp exists and has the proper permissions before
# checking for security updates
# https://github.com/digitalocean/marketplace-partners/issues/94
@ -10,11 +16,11 @@ chmod 1777 /tmp
export DEBIAN_FRONTEND=noninteractive
apt-get -y update
apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes
apt-get purge droplet-agent
apt-get -y purge droplet-agent
rm -rf /opt/digitalocean
apt-get -y autoremove
apt-get -y autoclean
rm -rf /tmp/* /var/tmp/*
history -c
cat /dev/null > /root/.bash_history
@ -36,12 +42,4 @@ The secure erase will complete successfully when you see:${NC}
dd: writing to '/zerofile': No space left on device\n
Beginning secure erase now\n"
dd if=/dev/zero of=/zerofile &
PID=$!
while [ -d /proc/$PID ]
do
printf "."
sleep 5
done
sync; rm /zerofile; sync
cat /dev/null > /var/log/lastlog; cat /dev/null > /var/log/wtmp
dd if=/dev/zero of=/zerofile bs=4096 || rm /zerofile

@ -4,7 +4,7 @@
# © 2021-2022 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
VERSION="v. 1.8"
VERSION="v. 1.8.1"
RUNDATE=$( date )
# Script should be run with SUDO
@ -75,7 +75,7 @@ function checkAgent {
echo -en "\e[41m[FAIL]\e[0m DigitalOcean directory detected.\n"
((FAIL++))
STATUS=2
if [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then
if [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]] || [[ $OS == "CloudLinux" ]]; then
echo "To uninstall the agent: 'sudo yum remove droplet-agent'"
echo "To remove the DO directory: 'find /opt/digitalocean/ -type d -empty -delete'"
elif [[ $OS == "Ubuntu" ]] || [[ $OS == "Debian" ]]; then
@ -357,7 +357,7 @@ function checkFirewall {
# shellcheck disable=SC2031
((WARN++))
fi
elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then
elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]] || [[ $OS == "CloudLinux" ]]; then
if [ -f /usr/lib/systemd/system/csf.service ]; then
fw="csf"
if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then
@ -456,7 +456,7 @@ function checkUpdates {
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n"
((PASS++))
fi
elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then
elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]] || [[ $OS == "CloudLinux" ]]; then
echo -en "\nChecking for available security updates, this may take a minute...\n\n"
update_count=$(yum check-update --security --quiet | wc -l)
@ -506,7 +506,7 @@ osv=0
if [[ $OS == "Ubuntu" ]]; then
ost=1
if [[ $VER == "22.04" ]] || [[ $VER == "20.04" ]] || [[ $VER == "18.04" ]] || [[ $VER == "16.04" ]]; then
if [[ $VER == "24.04" ]] || [[ $VER == "22.10" ]] || [[ $VER == "22.04" ]] || [[ $VER == "20.04" ]] || [[ $VER == "18.04" ]] || [[ $VER == "16.04" ]]; then
osv=1
fi
@ -521,7 +521,13 @@ elif [[ "$OS" =~ Debian.* ]]; then
;;
11)
osv=1
;;
;;
12)
osv=1
;;
13)
osv=1
;;
*)
osv=2
;;
@ -542,12 +548,28 @@ elif [[ $OS == "CentOS Stream" ]]; then
ost=1
if [[ $VER == "8" ]]; then
osv=1
elif [[ $VER == "9" ]]; then
osv=1
else
osv=2
fi
elif [[ $OS == "Rocky Linux" ]]; then
ost=1
if [[ $VER =~ 8\. ]]; then
if [[ $VER =~ 8\. ]] || [[ $VER =~ 9\. ]]; then
osv=1
else
osv=2
fi
elif [[ $OS == "AlmaLinux" ]]; then
ost=1
if [[ "$VER" =~ 8.* ]] || [[ "$VER" =~ 9.* ]]; then
osv=1
else
osv=2
fi
elif [[ $OS == "CloudLinux" ]]; then
ost=1
if [[ "$VER" =~ 8.* ]] || [[ "$VER" =~ 9.* ]]; then
osv=1
else
osv=2
@ -599,6 +621,12 @@ checkRoot
checkAgent
# Source GPU compatibility check
if [ -f "$(dirname "$0")/check_gpu_support.sh" ]; then
source "$(dirname "$0")/check_gpu_support.sh"
else
echo "GPU check script not found. Skipping GPU compatibility checks."
fi
# Summary
echo -en "\n\n---------------------------------------------------------------------------------------------------\n"
@ -625,4 +653,4 @@ elif [[ $STATUS == 1 ]]; then
else
echo -en "Some critical tests failed. These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n"
exit 1
fi
fi

@ -52,7 +52,7 @@ html
script(
crossorigin='anonymous'
src='https://polyfill.io/v3/polyfill.min.js?features=EventSource'
src='https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=EventSource'
)
//- JS

@ -1,17 +1,18 @@
{
"name": "wiki",
"version": "2.0.0",
"releaseDate": "2019-01-01T01:01:01.000Z",
"releaseDate": "2026-01-01T01:01:01.000Z",
"description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown",
"main": "wiki.js",
"dev": true,
"scripts": {
"start": "node server",
"dev": "node dev",
"build": "webpack --profile --config dev/webpack/webpack.prod.js",
"watch": "webpack --config dev/webpack/webpack.dev.js",
"dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider node dev",
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --profile --config dev/webpack/webpack.prod.js",
"watch": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --config dev/webpack/webpack.dev.js",
"test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
"cypress:open": "cypress open"
"cypress:open": "cypress open",
"postinstall": "patch-package"
},
"repository": {
"type": "git",
@ -33,11 +34,10 @@
},
"homepage": "https://github.com/Requarks/wiki#readme",
"engines": {
"node": ">=10.12"
"node": ">=20"
},
"dependencies": {
"asciidoctor": "2.2.6",
"@azure/storage-blob": "12.12.0",
"@azure/storage-blob": "12.29.1",
"@exlinc/keycloak-passport": "1.0.2",
"@joplin/turndown-plugin-gfm": "1.0.45",
"@root/csr": "0.8.1",
@ -49,8 +49,9 @@
"apollo-fetch": "0.7.0",
"apollo-server": "2.25.2",
"apollo-server-express": "2.25.2",
"asciidoctor": "2.2.6",
"auto-load": "3.0.4",
"aws-sdk": "2.1125.0",
"aws-sdk": "2.1693.0",
"azure-search-client": "3.1.5",
"bcryptjs-then": "1.0.1",
"bluebird": "3.7.2",
@ -59,26 +60,27 @@
"cheerio": "1.0.0-rc.5",
"chokidar": "3.5.3",
"chromium-pickle-js": "0.2.0",
"clean-css": "4.2.3",
"clean-css": "5.3.3",
"command-exists": "1.2.9",
"compression": "1.7.4",
"compression": "1.8.1",
"connect-session-knex": "2.0.0",
"cookie-parser": "1.4.6",
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"cuint": "0.2.2",
"custom-error-instance": "2.1.2",
"dependency-graph": "0.9.0",
"dependency-graph": "0.11.0",
"diff": "4.0.2",
"diff2html": "3.1.14",
"dompurify": "2.2.7",
"dompurify": "3.3.1",
"dotize": "0.3.0",
"elasticsearch6": "npm:@elastic/elasticsearch@6",
"elasticsearch7": "npm:@elastic/elasticsearch@7",
"emoji-regex": "9.2.2",
"elasticsearch8": "npm:@elastic/elasticsearch@8",
"emoji-regex": "10.2.1",
"eventemitter2": "6.4.9",
"express": "4.18.2",
"express-brute": "1.0.1",
"express-session": "1.17.3",
"express-session": "1.18.2",
"file-type": "15.0.1",
"filesize": "6.1.0",
"fs-extra": "9.0.1",
@ -94,11 +96,11 @@
"i18next-express-middleware": "2.0.0",
"i18next-node-fs-backend": "2.1.3",
"image-size": "0.9.2",
"js-base64": "3.7.2",
"js-base64": "3.7.8",
"js-binary": "1.2.0",
"js-yaml": "3.14.0",
"jsdom": "16.4.0",
"jsonwebtoken": "8.5.1",
"jsonwebtoken": "9.0.3",
"katex": "0.12.0",
"klaw": "3.0.0",
"knex": "0.21.7",
@ -108,7 +110,7 @@
"markdown-it-abbr": "1.0.4",
"markdown-it-attrs": "3.0.3",
"markdown-it-decorate": "1.2.2",
"markdown-it-emoji": "1.4.0",
"markdown-it-emoji": "3.0.0",
"markdown-it-expand-tabs": "1.0.13",
"markdown-it-external-links": "0.0.6",
"markdown-it-footnote": "3.0.3",
@ -116,26 +118,27 @@
"markdown-it-mark": "3.0.1",
"markdown-it-mathjax": "2.0.0",
"markdown-it-multimd-table": "4.0.3",
"markdown-it-pivot-table": "1.0.5",
"markdown-it-sub": "1.0.0",
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"mathjax": "3.1.2",
"mathjax": "3.2.2",
"mime-types": "2.1.35",
"moment": "2.29.4",
"moment-timezone": "0.5.38",
"moment": "2.30.1",
"moment-timezone": "0.6.0",
"mongodb": "3.6.5",
"ms": "2.1.3",
"mssql": "6.2.3",
"multer": "1.4.4",
"mysql2": "2.3.3",
"mysql2": "3.16.0",
"nanoid": "3.2.0",
"node-2fa": "1.1.2",
"node-cache": "5.1.2",
"nodemailer": "6.8.0",
"nodemailer": "6.9.1",
"objection": "2.2.18",
"passport": "0.4.1",
"passport-auth0": "1.4.3",
"passport-azure-ad": "4.3.4",
"passport-auth0": "1.4.5",
"passport-azure-ad": "4.3.5",
"passport-cas": "0.1.1",
"passport-discord": "0.1.4",
"passport-dropbox-oauth2": "1.1.0",
@ -143,50 +146,52 @@
"passport-github2": "0.1.12",
"passport-gitlab2": "5.0.0",
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0",
"passport-jwt": "4.0.1",
"passport-ldapauth": "3.0.1",
"passport-local": "1.0.0",
"passport-microsoft": "0.1.0",
"passport-oauth2": "1.6.1",
"passport-oauth2": "1.8.0",
"passport-okta-oauth": "0.0.1",
"passport-openidconnect": "0.1.1",
"passport-openidconnect": "0.1.2",
"passport-saml": "3.2.4",
"passport-slack-oauth2": "1.1.1",
"passport-slack-oauth2": "1.2.0",
"passport-twitch-strategy": "2.2.0",
"patch-package": "8.0.1",
"pem-jwk": "2.0.0",
"pg": "8.8.0",
"pg": "8.16.3",
"pg-hstore": "2.3.4",
"pg-pubsub": "0.5.0",
"pg-query-stream": "3.3.1",
"pg-tsquery": "8.1.0",
"pug": "3.0.2",
"punycode": "2.1.1",
"pg-pubsub": "0.8.1",
"pg-query-stream": "4.10.3",
"pg-tsquery": "8.4.2",
"postinstall-postinstall": "2.1.0",
"pug": "3.0.3",
"punycode": "2.3.1",
"qr-image": "3.2.0",
"raven": "2.6.4",
"remove-markdown": "0.3.0",
"remove-markdown": "0.6.2",
"request": "2.88.2",
"request-promise": "4.2.6",
"safe-regex": "2.1.1",
"sanitize-filename": "1.6.3",
"scim-query-filter-parser": "2.0.4",
"semver": "7.3.8",
"serve-favicon": "2.5.0",
"simple-git": "2.21.0",
"semver": "7.7.3",
"serve-favicon": "2.5.1",
"simple-git": "3.30.0",
"solr-node": "1.2.1",
"sqlite3": "5.0.6",
"ssh2": "1.5.0",
"sqlite3": "5.1.7",
"ssh2": "1.11.0",
"ssh2-promise": "1.0.3",
"striptags": "3.2.0",
"subscriptions-transport-ws": "0.9.18",
"tar-fs": "2.1.1",
"turndown": "7.1.1",
"twemoji": "13.1.0",
"turndown": "7.2.2",
"twemoji": "14.0.2",
"uslug": "1.0.4",
"uuid": "8.3.2",
"uuid": "9.0.0",
"validate.js": "0.13.1",
"winston": "3.3.3",
"xss": "1.0.14",
"yargs": "16.1.0"
"winston": "3.8.2",
"xss": "1.0.15",
"yargs": "17.6.2"
},
"devDependencies": {
"@babel/cli": "^7.12.1",
@ -214,7 +219,7 @@
"apollo-link-batch-http": "1.2.14",
"apollo-link-error": "1.1.13",
"apollo-link-http": "1.5.17",
"apollo-link-persisted-queries": "0.2.2",
"apollo-link-persisted-queries": "0.2.5",
"apollo-link-ws": "1.0.20",
"apollo-utilities": "1.3.4",
"autoprefixer": "9.8.6",
@ -227,7 +232,7 @@
"babel-plugin-transform-imports": "2.0.0",
"cache-loader": "4.1.0",
"canvas-confetti": "1.3.1",
"cash-dom": "8.1.1",
"cash-dom": "8.1.3",
"chart.js": "2.9.4",
"clean-webpack-plugin": "3.0.0",
"clipboard": "2.0.11",
@ -235,6 +240,7 @@
"codemirror-asciidoc": "1.0.4",
"copy-webpack-plugin": "6.2.1",
"core-js": "3.6.5",
"cross-env": "10.0.0",
"css-loader": "4.3.0",
"cssnano": "4.1.10",
"cypress": "5.3.0",
@ -252,7 +258,7 @@
"eslint-plugin-vue": "7.1.0",
"file-loader": "6.1.1",
"filepond": "4.21.1",
"filepond-plugin-file-validate-type": "1.2.7",
"filepond-plugin-file-validate-type": "1.2.8",
"filesize.js": "2.0.0",
"graphql-persisted-document-loader": "2.0.0",
"graphql-tag": "2.11.0",
@ -279,7 +285,7 @@
"postcss-import": "12.0.1",
"postcss-loader": "3.0.0",
"postcss-preset-env": "6.7.0",
"postcss-selector-parser": "6.0.10",
"postcss-selector-parser": "6.0.11",
"prismjs": "1.22.0",
"pug-lint": "2.6.0",
"pug-loader": "2.4.0",
@ -320,7 +326,7 @@
"webpack-bundle-analyzer": "3.9.0",
"webpack-cli": "3.3.12",
"webpack-dev-middleware": "3.7.2",
"webpack-hot-middleware": "2.25.1",
"webpack-hot-middleware": "2.25.3",
"webpack-merge": "5.2.0",
"webpack-modernizr-loader": "5.0.0",
"webpack-subresource-integrity": "1.5.1",
@ -332,7 +338,8 @@
},
"resolutions": {
"apollo-server-express/**/graphql-tools": "4.0.8",
"graphql": "15.3.0"
"graphql": "15.3.0",
"passport-saml/**/xml-crypto": "2.1.6"
},
"browserslist": [
"> 1%",

@ -0,0 +1,14 @@
diff --git a/node_modules/extract-files/package.json b/node_modules/extract-files/package.json
index 636fa03..1b75f79 100644
--- a/node_modules/extract-files/package.json
+++ b/node_modules/extract-files/package.json
@@ -34,6 +34,9 @@
"import": "./public/index.mjs",
"require": "./public/index.js"
},
+ "./public/extractFiles": "./public/extractFiles.js",
+ "./public/isExtractableFile": "./public/isExtractableFile.js",
+ "./public/ReactNativeFile": "./public/ReactNativeFile.js",
"./public/": "./public/",
"./package": "./package.json",
"./package.json": "./package.json"

@ -44,6 +44,7 @@ defaults:
title: Wiki.js
company: ''
contentLicense: ''
footerOverride: ''
logoUrl: https://static.requarks.io/logo/wikijs-butterfly.svg
pageExtensions:
- md
@ -84,7 +85,7 @@ defaults:
securityOpenRedirect: true
securityIframe: true
securityReferrerPolicy: true
securityTrustProxy: true
securityTrustProxy: false
securitySRI: true
securityHSTS: false
securityHSTSDuration: 300

@ -4,8 +4,8 @@ const express = require('express')
const ExpressBrute = require('express-brute')
const BruteKnex = require('../helpers/brute-knex')
const router = express.Router()
const moment = require('moment')
const _ = require('lodash')
const commonHelper = require('../helpers/common')
const bruteforce = new ExpressBrute(new BruteKnex({
createTable: true,
@ -70,19 +70,25 @@ router.all('/login/:strategy/callback', async (req, res, next) => {
const authResult = await WIKI.models.users.login({
strategy: req.params.strategy
}, { req, res })
res.cookie('jwt', authResult.jwt, { expires: moment().add(1, 'y').toDate() })
res.cookie('jwt', authResult.jwt, commonHelper.getCookieOpts())
const loginRedirect = req.cookies['loginRedirect']
const isValidRedirect = loginRedirect && loginRedirect.startsWith('/') && !loginRedirect.startsWith('//') && !loginRedirect.includes('://')
if (loginRedirect === '/' && authResult.redirect) {
res.clearCookie('loginRedirect')
res.redirect(authResult.redirect)
} else if (loginRedirect) {
} else if (isValidRedirect) {
res.clearCookie('loginRedirect')
res.redirect(loginRedirect)
} else if (authResult.redirect) {
res.redirect(authResult.redirect)
} else {
res.redirect('/')
if (loginRedirect) {
res.clearCookie('loginRedirect')
}
if (authResult.redirect) {
res.redirect(authResult.redirect)
} else {
res.redirect('/')
}
}
} catch (err) {
next(err)
@ -94,8 +100,7 @@ router.all('/login/:strategy/callback', async (req, res, next) => {
*/
router.post('/login', bruteforce.prevent, async (req, res, next) => {
_.set(res.locals, 'pageMeta.title', 'Login')
if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) {
if (req.query.legacy || (req.get('user-agent') && req.get('user-agent').indexOf('Trident') >= 0)) {
try {
const authResult = await WIKI.models.users.login({
strategy: req.body.strategy,
@ -103,7 +108,7 @@ router.post('/login', bruteforce.prevent, async (req, res, next) => {
password: req.body.pass
}, { req, res })
req.brute.reset()
res.cookie('jwt', authResult.jwt, { expires: moment().add(1, 'y').toDate() })
res.cookie('jwt', authResult.jwt, commonHelper.getCookieOpts())
res.redirect('/')
} catch (err) {
const { formStrategies, socialStrategies } = await WIKI.models.authentication.getStrategiesForLegacyClient()
@ -153,7 +158,7 @@ router.get('/verify/:token', bruteforce.prevent, async (req, res, next) => {
res.redirect('/login')
} else {
const result = await WIKI.models.users.refreshToken(usr)
res.cookie('jwt', result.token, { expires: moment().add(1, 'years').toDate() })
res.cookie('jwt', result.token, commonHelper.getCookieOpts())
res.redirect('/')
}
} catch (err) {

@ -4,6 +4,7 @@ const pageHelper = require('../helpers/page')
const _ = require('lodash')
const CleanCSS = require('clean-css')
const moment = require('moment')
const qs = require('querystring')
/* global WIKI */
@ -74,12 +75,12 @@ router.get(['/d', '/d/*'], async (req, res, next) => {
if (versionId > 0) {
if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'downloadVersion' })
return res.status(403).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' })
return res.status(403).render('unauthorized', { action: 'download' })
}
}
@ -141,7 +142,7 @@ router.get(['/e', '/e/*'], async (req, res, next) => {
// -> EDIT MODE
if (!(effectivePermissions.pages.write || effectivePermissions.pages.manage)) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'edit' })
return res.status(403).render('unauthorized', { action: 'edit' })
}
// -> Get page tags
@ -165,7 +166,7 @@ router.get(['/e', '/e/*'], async (req, res, next) => {
// -> CREATE MODE
if (!effectivePermissions.pages.write) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'create' })
return res.status(403).render('unauthorized', { action: 'create' })
}
_.set(res.locals, 'pageMeta.title', `New Page`)
@ -205,7 +206,7 @@ router.get(['/e', '/e/*'], async (req, res, next) => {
}
if (!WIKI.auth.checkAccess(req.user, ['read:history'], { path: pageVersion.path, locale: pageVersion.locale })) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'sourceVersion' })
return res.status(403).render('unauthorized', { action: 'sourceVersion' })
}
page.content = Buffer.from(pageVersion.content).toString('base64')
page.editorKey = pageVersion.editor
@ -220,7 +221,7 @@ router.get(['/e', '/e/*'], async (req, res, next) => {
}
if (!WIKI.auth.checkAccess(req.user, ['read:source'], { path: pageOriginal.path, locale: pageOriginal.locale })) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'source' })
return res.status(403).render('unauthorized', { action: 'source' })
}
page.content = Buffer.from(pageOriginal.content).toString('base64')
page.editorKey = pageOriginal.editorKey
@ -303,7 +304,7 @@ router.get(['/i', '/i/:id'], async (req, res, next) => {
tags: page.tags
})) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'view' })
return res.status(403).render('unauthorized', { action: 'view' })
}
if (WIKI.config.lang.namespacing) {
@ -318,7 +319,7 @@ router.get(['/i', '/i/:id'], async (req, res, next) => {
*/
router.get(['/p', '/p/*'], (req, res, next) => {
if (!req.user || req.user.id < 1 || req.user.id === 2) {
return res.render('unauthorized', { action: 'view' })
return res.status(403).render('unauthorized', { action: 'view' })
}
_.set(res.locals, 'pageMeta.title', 'User Profile')
@ -354,12 +355,12 @@ router.get(['/s', '/s/*'], async (req, res, next) => {
if (versionId > 0) {
if (!effectivePermissions.history.read) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'sourceVersion' })
return res.status(403).render('unauthorized', { action: 'sourceVersion' })
}
} else {
if (!effectivePermissions.source.read) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'source' })
return res.status(403).render('unauthorized', { action: 'source' })
}
}
@ -420,7 +421,8 @@ router.get('/*', async (req, res, next) => {
if (isPage) {
if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
return res.redirect(`/${pageArgs.locale}/${pageArgs.path}`)
const query = !_.isEmpty(req.query) ? `?${qs.stringify(req.query)}` : ''
return res.redirect(`/${pageArgs.locale}/${pageArgs.path}${query}`)
}
req.i18n.changeLanguage(pageArgs.locale)
@ -505,7 +507,7 @@ router.get('/*', async (req, res, next) => {
injectCode.body = `${injectCode.body}\n${page.extra.js}`
}
if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) {
if (req.query.legacy || (req.get('user-agent') && req.get('user-agent').indexOf('Trident') >= 0)) {
// -> Convert page TOC
if (_.isString(page.toc)) {
page.toc = JSON.parse(page.toc)

@ -28,8 +28,7 @@ router.get('/.well-known/acme-challenge/:token', (req, res, next) => {
*/
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}`)
return res.redirect(`https://${req.hostname}${req.originalUrl}`)
} else {
next()
}

@ -4,10 +4,11 @@ const _ = require('lodash')
const jwt = require('jsonwebtoken')
const ms = require('ms')
const { DateTime } = require('luxon')
const Promise = require('bluebird')
const crypto = Promise.promisifyAll(require('crypto'))
const crypto = require('crypto')
const pem2jwk = require('pem-jwk').pem2jwk
const randomBytesAsync = require('util').promisify(crypto.randomBytes)
const commonHelper = require('../helpers/common')
const securityHelper = require('../helpers/security')
/* global WIKI */
@ -82,7 +83,7 @@ module.exports = {
const strategy = require(`../modules/authentication/${stg.strategyKey}/authentication.js`)
stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`
stg.config.key = stg.key;
stg.config.key = stg.key
strategy.init(passport, stg.config)
strategy.config = stg.config
@ -154,8 +155,11 @@ module.exports = {
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() })
res.cookie('jwt', newToken.token, commonHelper.getCookieOpts())
}
// Avoid caching this response
res.set('Cache-Control', 'no-store')
} catch (errc) {
WIKI.logger.warn(errc)
return next()
@ -313,6 +317,49 @@ module.exports = {
return true
},
/**
* Check if user (requester) can perform user assignment to a group with elevated permissions
*
* @param {User} requester The user attempting to perform the assignment
* @param {Array<Number>} groupIds List of group IDs to be assigned
* @returns {Boolean}
*/
async checkAssignUserToGroupAccess(requester, groupIds = []) {
if (!groupIds || groupIds.length < 1) {
return true
}
const requesterPermissions = requester.permissions ? requester.permissions : requester.getGlobalPermissions()
// System Admin
if (requesterPermissions.includes('manage:system')) {
return true
}
// Ensure basic user management permission
if (!requesterPermissions.some(p => ['write:users', 'manage:users', 'write:groups', 'manage:groups'].includes(p))) {
return false
}
const groups = await WIKI.models.groups.query().whereIn('id', groupIds)
return groups.every(grp => {
// Check group for manage:system permission
if (grp.permissions.includes('manage:system')) {
return false
}
// Check group for administrative permissions
if (grp.permissions.some(p => {
const permType = _.last(p.split(':'))
return ['users', 'groups', 'navigation', 'theme', 'api'].includes(permType)
}) && !requesterPermissions.includes('manage:groups')) {
return false
}
return true
})
},
/**
* Check and apply Page Rule specificity
*
@ -363,7 +410,7 @@ module.exports = {
async regenerateCertificates () {
WIKI.logger.info('Regenerating certificates...')
_.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
_.set(WIKI.config, 'sessionSecret', (await randomBytesAsync(32)).toString('hex'))
const certs = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {

@ -91,6 +91,13 @@ module.exports = {
dbConfig.ssl = sslOptions
}
// Prune host and port if socketPath is configured
if (WIKI.config.db.socketPath) {
const { host, port, ...prunedConfig } = dbConfig
dbConfig = prunedConfig
dbConfig.socketPath = WIKI.config.db.socketPath.toString()
}
// Fix mysql boolean handling...
dbConfig.typeCast = (field, next) => {
if (field.type === 'TINY' && field.length === 1) {
@ -138,7 +145,7 @@ module.exports = {
switch (WIKI.config.db.type) {
case 'postgres':
await conn.query(`set application_name = 'Wiki.js'`)
// -> Set schema if it's not public
// -> Set schema if it's not public
if (WIKI.config.db.schema && WIKI.config.db.schema !== 'public') {
await conn.query(`set search_path TO ${WIKI.config.db.schema}, public;`)
}
@ -222,7 +229,7 @@ module.exports = {
* Subscribe to database LISTEN / NOTIFY for multi-instances events
*/
async subscribeToNotifications () {
const useHA = (WIKI.config.ha === true || WIKI.config.ha === 'true' || WIKI.config.ha === 1 || WIKI.config.ha === '1')
const useHA = (WIKI.config.ha === true || (typeof WIKI.config.ha === 'string' && WIKI.config.ha.toLowerCase() === 'true') || WIKI.config.ha === 1 || WIKI.config.ha === '1')
if (!useHA) {
return
} else if (WIKI.config.db.type !== 'postgres') {

@ -4,6 +4,8 @@ const https = require('https')
const { ApolloServer } = require('apollo-server-express')
const Promise = require('bluebird')
const _ = require('lodash')
const jwt = require('jsonwebtoken')
const cookie = require('cookie')
/* global WIKI */
@ -125,7 +127,35 @@ module.exports = {
context: ({ req, res }) => ({ req, res }),
subscriptions: {
onConnect: (connectionParams, webSocket) => {
let token = _.get(connectionParams, 'token', null)
if (!token) {
const cookieHeader = _.get(webSocket, 'upgradeReq.headers.cookie', '')
if (cookieHeader) {
const cookies = cookie.parse(cookieHeader)
token = cookies.jwt || null
}
}
if (!token) {
throw new Error('Unauthorized')
}
try {
const user = jwt.verify(token, WIKI.config.certs.public, {
audience: WIKI.config.auth.audience,
issuer: 'urn:wiki.js',
algorithms: ['RS256']
})
if (!_.includes(user.permissions, 'manage:system')) {
throw new Error('Forbidden')
}
return { user }
} catch (err) {
throw new Error('Unauthorized')
}
},
path: '/graphql-subscriptions'
}

@ -4,8 +4,8 @@ const Promise = require('bluebird')
const fs = require('fs-extra')
const path = require('path')
const zlib = require('zlib')
const stream = require('stream')
const pipeline = Promise.promisify(stream.pipeline)
const { pipeline } = require('node:stream/promises')
const { Readable, Transform } = require('node:stream')
/* global WIKI */
@ -121,7 +121,7 @@ module.exports = {
await pipeline(
WIKI.models.knex.select('filename', 'folderId', 'data').from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),
new stream.Transform({
new Transform({
objectMode: true,
transform: async (asset, enc, cb) => {
const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename
@ -150,7 +150,7 @@ module.exports = {
const commentsProgressMultiplier = progressMultiplier / Math.ceil(commentsCount / 50)
WIKI.logger.info(`Found ${commentsCount} comments to export. Streaming to file...`)
const rs = stream.Readable({ objectMode: true })
const rs = Readable({ objectMode: true })
rs._read = () => {}
const fetchCommentsBatch = async (offset) => {
@ -177,7 +177,7 @@ module.exports = {
let marker = 0
await pipeline(
rs,
new stream.Transform({
new Transform({
objectMode: true,
transform (chunk, encoding, callback) {
marker++
@ -225,7 +225,7 @@ module.exports = {
const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10)
WIKI.logger.info(`Found ${pagesCount} pages history to export. Streaming to file...`)
const rs = stream.Readable({ objectMode: true })
const rs = Readable({ objectMode: true })
rs._read = () => {}
const fetchPagesBatch = async (offset) => {
@ -255,7 +255,7 @@ module.exports = {
let marker = 0
await pipeline(
rs,
new stream.Transform({
new Transform({
objectMode: true,
transform (chunk, encoding, callback) {
marker++
@ -307,7 +307,7 @@ module.exports = {
const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10)
WIKI.logger.info(`Found ${pagesCount} pages to export. Streaming to file...`)
const rs = stream.Readable({ objectMode: true })
const rs = Readable({ objectMode: true })
rs._read = () => {}
const fetchPagesBatch = async (offset) => {
@ -337,7 +337,7 @@ module.exports = {
let marker = 0
await pipeline(
rs,
new stream.Transform({
new Transform({
objectMode: true,
transform (chunk, encoding, callback) {
marker++
@ -400,7 +400,7 @@ module.exports = {
const usersProgressMultiplier = progressMultiplier / Math.ceil(usersCount / 50)
WIKI.logger.info(`Found ${usersCount} users to export. Streaming to file...`)
const rs = stream.Readable({ objectMode: true })
const rs = Readable({ objectMode: true })
rs._read = () => {}
const fetchUsersBatch = async (offset) => {
@ -427,7 +427,7 @@ module.exports = {
let marker = 0
await pipeline(
rs,
new stream.Transform({
new Transform({
objectMode: true,
transform (chunk, encoding, callback) {
marker++

@ -45,15 +45,23 @@ module.exports = {
throw new gql.GraphQLError('Invalid Group ID')
}
// Check assigned permissions for write:groups
// Check assigned permissions for manage:users / write:groups
if (
WIKI.auth.checkExclusiveAccess(req.user, ['write:groups'], ['manage:groups', 'manage:system']) &&
WIKI.auth.checkExclusiveAccess(req.user, ['manage:users', '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 gql.GraphQLError('You are not authorized to assign a user to this elevated group.')
throw new gql.GraphQLError('You are not authorized to assign a user to this administrative group.')
}
// Check assigned permissions for manage:groups
if (
WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage:system']) &&
grp.permissions.some(p => _.last(p.split(':')) === 'system')
) {
throw new gql.GraphQLError('You are not authorized to assign a user to a group with the manage:system permission.')
}
// Check for valid user
@ -170,7 +178,7 @@ module.exports = {
return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType)
})
) {
throw new gql.GraphQLError('You are not authorized to manage this group or assign these permissions.')
throw new gql.GraphQLError('You are not authorized to manage this group or assign these administrative permissions.')
}
// Check assigned permissions for manage:groups

@ -170,6 +170,30 @@ module.exports = {
throw new WIKI.Error.PageNotFound()
}
},
async singleByPath(obj, args, context, info) {
let page = await WIKI.models.pages.getPageFromDb({
path: args.path,
locale: args.locale,
});
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['manage:pages', 'delete:pages'], {
path: page.path,
locale: page.localeCode
})) {
return {
...page,
locale: page.localeCode,
editor: page.editorKey,
scriptJs: page.extra.js,
scriptCss: page.extra.css
}
} else {
throw new WIKI.Error.PageViewForbidden()
}
} else {
throw new WIKI.Error.PageNotFound()
}
},
/**
* FETCH TAGS
*/

@ -17,6 +17,7 @@ module.exports = {
title: WIKI.config.title,
company: WIKI.config.company,
contentLicense: WIKI.config.contentLicense,
footerOverride: WIKI.config.footerOverride,
logoUrl: WIKI.config.logoUrl,
pageExtensions: WIKI.config.pageExtensions.join(', '),
...WIKI.config.seo,
@ -60,6 +61,10 @@ module.exports = {
WIKI.config.contentLicense = args.contentLicense
}
if (args.hasOwnProperty('footerOverride')) {
WIKI.config.footerOverride = args.footerOverride
}
if (args.hasOwnProperty('logoUrl')) {
WIKI.config.logoUrl = _.trim(args.logoUrl)
}
@ -120,7 +125,7 @@ module.exports = {
forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload)
}
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'pageExtensions', 'auth', 'editShortcuts', 'features', 'security', 'uploads'])
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'footerOverride', 'seo', 'logoUrl', 'pageExtensions', 'auth', 'editShortcuts', 'features', 'security', 'uploads'])
if (WIKI.config.security.securityTrustProxy) {
WIKI.app.enable('trust proxy')

@ -1,6 +1,5 @@
const _ = require('lodash')
const Promise = require('bluebird')
const getos = Promise.promisify(require('getos'))
const getos = require('getos')
const os = require('os')
const filesize = require('filesize')
const path = require('path')
@ -11,6 +10,8 @@ const request = require('request-promise')
const crypto = require('crypto')
const nanoid = require('nanoid/non-secure').customAlphabet('1234567890abcdef', 10)
const getosAsync = require('util').promisify(getos)
/* global WIKI */
const dbTypes = {
@ -371,7 +372,7 @@ module.exports = {
async operatingSystem () {
let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`
if (os.platform() === 'linux') {
const osInfo = await getos()
const osInfo = await getosAsync()
osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`
}
return osLabel

@ -62,8 +62,12 @@ module.exports = {
}
},
UserMutation: {
async create (obj, args) {
async create (obj, args, context) {
try {
if (!(await WIKI.auth.checkAssignUserToGroupAccess(context.req.user, args.groups))) {
throw new Error('You are not authorized to create a user with an assignment to an administrative group.')
}
await WIKI.models.users.createNewUser(args)
return {
@ -94,12 +98,16 @@ module.exports = {
}
}
},
async update (obj, args) {
async update (obj, args, context) {
try {
if (!(await WIKI.auth.checkAssignUserToGroupAccess(context.req.user, args.groups))) {
throw new Error('You are not authorized to modify / assign a user from / to an administrative group.')
}
await WIKI.models.users.updateUser(args)
return {
responseResult: graphHelper.generateSuccess('User created successfully')
responseResult: graphHelper.generateSuccess('User updated successfully')
}
} catch (err) {
return graphHelper.generateError(err)

@ -18,7 +18,7 @@ type GroupQuery {
list(
filter: String
orderBy: String
): [GroupMinimal] @auth(requires: ["write:groups", "manage:groups", "manage:system"])
): [GroupMinimal] @auth(requires: ["write:users", "manage:users", "write:groups", "manage:groups", "manage:system"])
single(
id: Int!
@ -49,12 +49,12 @@ type GroupMutation {
assignUser(
groupId: Int!
userId: Int!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
): DefaultResponse @auth(requires: ["manage:users", "write:groups", "manage:groups", "manage:system"])
unassignUser(
groupId: Int!
userId: Int!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
): DefaultResponse @auth(requires: ["manage:users", "write:groups", "manage:groups", "manage:system"])
}
# -----------------------------------------------

@ -46,6 +46,11 @@ type PageQuery {
id: Int!
): Page @auth(requires: ["read:pages", "manage:system"])
singleByPath(
path: String!
locale: String!
): Page @auth(requires: ["read:pages", "manage:system"])
tags: [PageTag]! @auth(requires: ["manage:system", "read:pages"])
searchTags(

@ -32,6 +32,7 @@ type SiteMutation {
analyticsId: String
company: String
contentLicense: String
footerOverride: String
logoUrl: String
pageExtensions: String
authAutoLogin: Boolean
@ -81,6 +82,7 @@ type SiteConfig {
analyticsId: String
company: String
contentLicense: String
footerOverride: String
logoUrl: String
pageExtensions: String
authAutoLogin: Boolean

@ -1,4 +1,7 @@
/* global WIKI */
const _ = require('lodash')
const { DateTime } = require('luxon')
module.exports = {
/**
@ -38,5 +41,11 @@ module.exports = {
})
return result
}, {})
},
getCookieOpts () {
return {
expires: DateTime.utc().plus({ days: 365 }).toJSDate(),
...(WIKI.config.host.startsWith('https://') ? { secure: true } : {})
}
}
}

@ -6,6 +6,11 @@
const path = require('path')
const { nanoid } = require('nanoid')
const { DateTime } = require('luxon')
const { gte } = require('semver')
// ----------------------------------------
// Init WIKI instance
// ----------------------------------------
let WIKI = {
IS_DEBUG: process.env.NODE_ENV === 'development',

@ -149,11 +149,12 @@ module.exports = async () => {
title: WIKI.config.title,
theme: WIKI.config.theming.theme,
darkMode: WIKI.config.theming.darkMode,
tocPosition: WIKI.config.theming.tocPosition,
tocPosition: WIKI.config.theming.tocPosition || 'left',
lang: WIKI.config.lang.code,
rtl: WIKI.config.lang.rtl,
company: WIKI.config.company,
contentLicense: WIKI.config.contentLicense,
footerOverride: WIKI.config.footerOverride,
logoUrl: WIKI.config.logoUrl
}
res.locals.langs = await WIKI.models.locales.getNavLocales({ cache: true })

@ -92,6 +92,7 @@ module.exports = class Authentication extends Model {
}
for (const strategy of dbStrategies) {
let newProps = false
const strategyDef = _.find(WIKI.data.authentication, ['key', strategy.strategyKey])
if (!strategyDef) {
await WIKI.models.authentication.query().delete().where('key', strategy.key)
@ -101,6 +102,8 @@ module.exports = class Authentication extends Model {
strategy.config = _.transform(strategyDef.props, (result, value, key) => {
if (!_.has(result, key)) {
_.set(result, key, value.default)
// we have some new properties added to an existing auth strategy to write to the database
newProps = true
}
return result
}, strategy.config)
@ -111,6 +114,12 @@ module.exports = class Authentication extends Model {
displayName: strategyDef.title
}).where('key', strategy.key)
}
// write existing auth model to database with new properties and defaults
if (newProps) {
await WIKI.models.authentication.query().patch({
config: strategy.config
}).where('key', strategy.key)
}
}
WIKI.logger.info(`Loaded ${WIKI.data.authentication.length} authentication strategies: [ OK ]`)

@ -725,7 +725,7 @@ module.exports = class Page extends Model {
const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' })
// -> Move page
const destinationTitle = (page.title === page.path ? opts.destinationPath : page.title)
const destinationTitle = (page.title === _.last(page.path.split('/')) ? _.last(opts.destinationPath.split('/')) : page.title)
await WIKI.models.pages.query().patch({
path: opts.destinationPath,
localeCode: opts.destinationLocale,
@ -745,6 +745,7 @@ module.exports = class Page extends Model {
...page,
destinationPath: opts.destinationPath,
destinationLocaleCode: opts.destinationLocale,
title: destinationTitle,
destinationHash
})

@ -499,6 +499,10 @@ module.exports = class User extends Model {
})
if (usr) {
if (!usr.isActive) {
throw new WIKI.Error.AuthAccountBanned()
}
await WIKI.models.users.query().patch({
password: newPassword,
mustChangePwd: false
@ -527,6 +531,9 @@ module.exports = class User extends Model {
if (!usr) {
WIKI.logger.debug(`Password reset attempt on nonexistant local account ${email}: [DISCARDED]`)
return
} else if (!usr.isActive) {
WIKI.logger.debug(`Password reset attempt on disabled local account ${email}: [DISCARDED]`)
return
}
const resetToken = await WIKI.models.userKeys.generateToken({
userId: usr.id,
@ -866,7 +873,7 @@ module.exports = class User extends Model {
}
const usr = await WIKI.models.users.query().findById(context.req.user.id).select('providerKey')
const provider = _.find(WIKI.auth.strategies, ['key', usr.providerKey])
return provider.logout ? provider.logout(provider.config) : '/'
return provider.logout ? provider.logout(provider.config, context) : '/'
}
static async getGuestUser () {

@ -9,5 +9,5 @@ props:
propertyTrackingId:
type: String
title: Property Tracking ID
hint: UA-XXXXXXX-X
hint: G-XXXXXXXXXX
order: 1

@ -1,5 +1,5 @@
key: umami
title: Umami Analytics
title: Umami Analytics v1
description: Umami is a simple, fast, privacy-focused alternative to Google Analytics.
author: CDN18
logo: https://static.requarks.io/logo/umami.svg

@ -0,0 +1,2 @@
head: |
<script async src="{{url}}/script.js" data-website-id="{{websiteID}}"></script>

@ -0,0 +1,17 @@
key: umami2
title: Umami Analytics v2
description: Umami is a simple, fast, privacy-focused alternative to Google Analytics.
author: CDN18
logo: https://static.requarks.io/logo/umami.svg
website: https://umami.is
isAvailable: true
props:
websiteID:
type: String
title: Website ID
order: 1
url:
type: String
title: Umami Server URL
hint: The URL of your Umami instance. It should start with http/https and omit the trailing slash. (e.g. https://umami.example.com)
order: 2

@ -48,6 +48,19 @@ module.exports = {
picture: ''
}
})
if (conf.mapGroups) {
const groups = _.get(profile, '_json.groups')
if (groups && _.isArray(groups)) {
const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)
const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)
for (const groupId of _.difference(expectedGroups, currentGroups)) {
await user.$relatedQuery('groups').relate(groupId)
}
for (const groupId of _.difference(currentGroups, expectedGroups)) {
await user.$relatedQuery('groups').unrelate().where('groupId', groupId)
}
}
}
cb(null, user)
} catch (err) {
cb(err, null)

@ -27,3 +27,9 @@ props:
title: Cookie Encryption Key String
hint: Random string with 44-character length. Setting this enables workaround for Chrome's SameSite cookies.
order: 3
mapGroups:
type: Boolean
title: Map Groups
hint: Map groups matching names from the groups claim value
default: false
order: 4

@ -27,6 +27,14 @@ module.exports = {
passport.use(conf.key,
new GitHubStrategy(githubConfig, async (req, accessToken, refreshToken, profile, cb) => {
try {
WIKI.logger.info(`GitHub OAuth: Processing profile for user ${profile.id || profile.username}`)
// Ensure email is available - passport-github2 should fetch it automatically with user:email scope
// but we'll log a warning if it's missing
if (!profile.emails || (Array.isArray(profile.emails) && profile.emails.length === 0)) {
WIKI.logger.warn(`GitHub OAuth: No email found in profile for user ${profile.id || profile.username}. Make sure 'user:email' scope is granted.`)
}
const user = await WIKI.models.users.processProfile({
providerKey: req.params.strategy,
profile: {
@ -34,9 +42,19 @@ module.exports = {
picture: _.get(profile, 'photos[0].value', '')
}
})
WIKI.logger.info(`GitHub OAuth: Successfully authenticated user ${user.email}`)
cb(null, user)
} catch (err) {
cb(err, null)
WIKI.logger.warn(`GitHub OAuth: Authentication failed for strategy ${req.params.strategy}:`, err)
// Provide more user-friendly error messages
if (err.message && err.message.includes('email')) {
cb(new Error('GitHub authentication failed: Email address is required but not available. Please ensure your GitHub account has a verified email address and grant email access permissions.'), null)
} else if (err instanceof WIKI.Error.AuthAccountBanned) {
cb(err, null)
} else {
cb(new Error(`GitHub authentication failed: ${err.message || 'Unknown error'}`), null)
}
}
}
))

@ -15,6 +15,8 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
baseURL: conf.baseUrl,
authorizationURL: conf.authorizationURL || (conf.baseUrl + '/oauth/authorize'),
tokenURL: conf.tokenURL || (conf.baseUrl + '/oauth/token'),
scope: ['read_user'],
passReqToCallback: true
}, async (req, accessToken, refreshToken, profile, cb) => {

@ -24,3 +24,13 @@ props:
hint: For self-managed GitLab instances, define the base URL (e.g. https://gitlab.example.com). Leave default for GitLab.com SaaS (https://gitlab.com).
default: https://gitlab.com
order: 3
authorizationURL:
type: String
title: Authorization URL
hint: For self-managed GitLab instances, define an alternate authorization URL (e.g. http://example.com/oauth/authorize). Leave empty otherwise.
order: 4
tokenURL:
type: String
title: Token URL
hint: For self-managed GitLab instances, define an alternate token URL (e.g. http://example.com/oauth/token). Leave empty otherwise.
order: 5

@ -16,9 +16,13 @@ module.exports = {
passReqToCallback: true
}, async (req, accessToken, refreshToken, profile, cb) => {
try {
if (conf.hostedDomain && conf.hostedDomain != profile._json.hd) {
throw new Error('Google authentication should have been performed with domain ' + conf.hostedDomain)
WIKI.logger.info(`Google OAuth: Processing profile for user ${profile.id || profile.displayName}`)
// Validate hosted domain if configured
if (conf.hostedDomain && profile._json.hd !== conf.hostedDomain) {
throw new Error(`Google authentication failed: User must be from domain ${conf.hostedDomain}, but got ${profile._json.hd || 'unknown'}`)
}
const user = await WIKI.models.users.processProfile({
providerKey: req.params.strategy,
profile: {
@ -26,9 +30,21 @@ module.exports = {
picture: _.get(profile, 'photos[0].value', '')
}
})
WIKI.logger.info(`Google OAuth: Successfully authenticated user ${user.email}`)
cb(null, user)
} catch (err) {
cb(err, null)
WIKI.logger.warn(`Google OAuth: Authentication failed for strategy ${req.params.strategy}:`, err)
// Provide more user-friendly error messages
if (err.message && err.message.includes('domain')) {
cb(new Error(`Google authentication failed: ${err.message}`), null)
} else if (err.message && err.message.includes('email')) {
cb(new Error('Google authentication failed: Email address is required but not available. Please ensure your Google account has a verified email address.'), null)
} else if (err instanceof WIKI.Error.AuthAccountBanned) {
cb(err, null)
} else {
cb(new Error(`Google authentication failed: ${err.message || 'Unknown error'}`), null)
}
}
})

@ -21,7 +21,7 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
passReqToCallback: true
}, async (req, accessToken, refreshToken, profile, cb) => {
}, async (req, accessToken, refreshToken, results, profile, cb) => {
let displayName = profile.username
if (_.isString(profile.fullName) && profile.fullName.length > 0) {
displayName = profile.fullName
@ -36,6 +36,7 @@ module.exports = {
picture: ''
}
})
req.session.keycloak_id_token = results.id_token
cb(null, user)
} catch (err) {
cb(err, null)
@ -43,11 +44,22 @@ module.exports = {
})
)
},
logout (conf) {
logout (conf, context) {
if (!conf.logoutUpstream) {
return '/'
} else if (conf.logoutURL && conf.logoutURL.length > 5) {
return `${conf.logoutURL}?redirect_uri=${encodeURIComponent(WIKI.config.host)}`
const idToken = context.req.session.keycloak_id_token
const redirURL = encodeURIComponent(WIKI.config.host)
if (conf.logoutUpstreamRedirectLegacy) {
// keycloak < 18
return `${conf.logoutURL}?redirect_uri=${redirURL}`
} else if (idToken) {
// keycloak 18+
return `${conf.logoutURL}?post_logout_redirect_uri=${redirURL}&id_token_hint=${idToken}`
} else {
// fall back to no redirect if keycloak_id_token isn't available
return conf.logoutURL
}
} else {
WIKI.logger.warn('Keycloak logout URL is not configured!')
return '/'

@ -35,17 +35,17 @@ props:
authorizationURL:
type: String
title: Authorization Endpoint URL
hint: e.g. https://KEYCLOAK-HOST/auth/realms/YOUR-REALM/protocol/openid-connect/auth
hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/auth
order: 5
tokenURL:
type: String
title: Token Endpoint URL
hint: e.g. https://KEYCLOAK-HOST/auth/realms/YOUR-REALM/protocol/openid-connect/token
hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/token
order: 6
userInfoURL:
type: String
title: User Info Endpoint URL
hint: e.g. https://KEYCLOAK-HOST/auth/realms/YOUR-REALM/protocol/openid-connect/userinfo
hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/userinfo
order: 7
logoutUpstream:
type: Boolean
@ -55,6 +55,11 @@ props:
logoutURL:
type: String
title: Logout Endpoint URL
hint: e.g. https://KEYCLOAK-HOST/auth/realms/YOUR-REALM/protocol/openid-connect/logout
hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/logout
order: 9
logoutUpstreamRedirectLegacy:
type: Boolean
title: Legacy Logout Redirect
hint: Pass the legacy 'redirect_uri' parameter to the logout endpoint. Leave disabled for Keycloak 18 and above.
order: 10

@ -19,6 +19,13 @@ module.exports = {
searchBase: conf.searchBase,
searchFilter: conf.searchFilter,
tlsOptions: getTlsOptions(conf),
...conf.mapGroups && {
groupSearchBase: conf.groupSearchBase,
groupSearchFilter: conf.groupSearchFilter,
groupSearchScope: conf.groupSearchScope,
groupDnProperty: conf.groupDnProperty,
groupSearchAttributes: [conf.groupNameField]
},
includeRaw: true
},
usernameField: 'email',
@ -40,6 +47,21 @@ module.exports = {
picture: _.get(profile, `_raw.${conf.mappingPicture}`, '')
}
})
// map users LDAP groups to wiki groups with the same name, and remove any groups that don't match LDAP
if (conf.mapGroups) {
const ldapGroups = _.get(profile, '_groups')
if (ldapGroups && _.isArray(ldapGroups)) {
const groups = ldapGroups.map(g => g[conf.groupNameField])
const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)
const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)
for (const groupId of _.difference(expectedGroups, currentGroups)) {
await user.$relatedQuery('groups').relate(groupId)
}
for (const groupId of _.difference(currentGroups, expectedGroups)) {
await user.$relatedQuery('groups').unrelate().where('groupId', groupId)
}
}
}
cb(null, user)
} catch (err) {
if (WIKI.config.flags.ldapdebug) {
@ -59,7 +81,7 @@ function getTlsOptions(conf) {
if (!conf.tlsCertPath) {
return {
rejectUnauthorized: conf.verifyTLSCertificate,
rejectUnauthorized: conf.verifyTLSCertificate
}
}

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

Loading…
Cancel
Save