mirror of
https://github.com/wekan/wekan.git
synced 2026-11-08 19:35:19 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b98272272 | ||
|
|
ee22fb1a3b | ||
|
|
154c9a6e52 | ||
|
|
121f37be91 | ||
|
|
79c40407d1 | ||
|
|
148d18d456 | ||
|
|
15877dd1bc | ||
|
|
3ccb465c3f | ||
|
|
71d9776b61 | ||
|
|
34d9817110 | ||
|
|
aa939d7bec | ||
|
|
194e5b0a51 | ||
|
|
532a96da11 | ||
|
|
ba0de93f79 | ||
|
|
9a15491985 | ||
|
|
c8ed3e0fd0 | ||
|
|
4a9c3f21d2 | ||
|
|
3a2fef4dc7 | ||
|
|
f241681e80 | ||
|
|
c71c875649 | ||
|
|
0da86171e9 | ||
|
|
b80e21fd48 | ||
|
|
24a7270b7f | ||
|
|
73ea323da1 | ||
|
|
be6727607c | ||
|
|
9c5fa760c3 | ||
|
|
27040c6f2f | ||
|
|
c15e1ab4b2 | ||
|
|
b676cdc169 | ||
|
|
4ad13db5ef | ||
|
|
efe0e9287c | ||
|
|
cfcead6a1a | ||
|
|
40f60582b7 | ||
|
|
96845f6f4b | ||
|
|
c5f5ad2338 | ||
|
|
0c8c254dc5 | ||
|
|
0a1f8584ee | ||
|
|
2042fd5102 | ||
|
|
0403e27c62 | ||
|
|
d9e48a909d | ||
|
|
0cea141702 | ||
|
|
9188843369 | ||
|
|
dbf07aadf9 | ||
|
|
e001c4f9c2 | ||
|
|
ffe207e191 | ||
|
|
b2198cb61b | ||
|
|
a6bec88266 | ||
|
|
8a1f7a14a9 | ||
|
|
7f948bf438 | ||
|
|
6d0ab6cf11 | ||
|
|
89b64966c6 | ||
|
|
d5e3325e87 | ||
|
|
897376ba54 | ||
|
|
16103f9ad2 | ||
|
|
8cbe2605fe | ||
|
|
f5eb4ecf61 | ||
|
|
8d4f4caa2f | ||
|
|
9a74138a84 | ||
|
|
fa7e447536 | ||
|
|
05cc2460ea | ||
|
|
5c50f38e25 | ||
|
|
338afc15f8 | ||
|
|
dd2082b0ea | ||
|
|
4a86e9c19e | ||
|
|
568cd9b6ec | ||
|
|
fea468471d | ||
|
|
83199b9af7 | ||
|
|
3839ee698d | ||
|
|
a60b996c92 | ||
|
|
1230164c16 | ||
|
|
8008f410fe | ||
|
|
bd90782da2 | ||
|
|
5eadc45563 | ||
|
|
3785a13f6a | ||
|
|
97cabdeca4 | ||
|
|
2802326888 | ||
|
|
1f46af0639 | ||
|
|
1bcb1da92f | ||
|
|
067efac7f8 | ||
|
|
2bdd16060e | ||
|
|
433bc62211 | ||
|
|
29f9c62a64 | ||
|
|
21766cf38d | ||
|
|
101cdb7579 | ||
|
|
54cf951d9c | ||
|
|
57a43ee8e1 | ||
|
|
ff2c0ed7f6 | ||
|
|
63d022fe5f | ||
|
|
e9cf825b22 | ||
|
|
4ce8514e36 | ||
|
|
429fdc5319 | ||
|
|
ffd5cbd2ad | ||
|
|
cf4b48df09 | ||
|
|
73c21dfe3e | ||
|
|
22a12dc7f8 | ||
|
|
b1403ca7af | ||
|
|
bc21c901f4 | ||
|
|
88fef5dc08 | ||
|
|
3f9379ac86 | ||
|
|
85af731c1b | ||
|
|
bb32a50550 | ||
|
|
c99aa7f845 | ||
|
|
508484ef1e | ||
|
|
548196c9a9 | ||
|
|
9a118c83b2 | ||
|
|
d442bd8a7e | ||
|
|
5a13e927bc | ||
|
|
c78af24a36 | ||
|
|
6551062492 | ||
|
|
a0ed5de6f1 | ||
|
|
f406d46fe7 | ||
|
|
4b0aa715eb | ||
|
|
f5af33ac38 | ||
|
|
f165a24192 | ||
|
|
e6ea524541 | ||
|
|
de23dece66 | ||
|
|
2017f2871f | ||
|
|
302b47db6f | ||
|
|
482c2c29f4 | ||
|
|
4d5b832499 | ||
|
|
41173f0f6b | ||
|
|
d7abe9910a | ||
|
|
48a2e97ea3 | ||
|
|
4993242c5f | ||
|
|
12eef31162 | ||
|
|
0b5c855820 | ||
|
|
0c53ef6960 | ||
|
|
2ab0afdc93 | ||
|
|
f2a1d7b9ba | ||
|
|
cd1f1b8f05 | ||
|
|
b941f36e4c | ||
|
|
487a528806 | ||
|
|
dc9a7bb1ac | ||
|
|
696a1a2c8a | ||
|
|
ef1e683cdd | ||
|
|
55163d0ce6 | ||
|
|
b974ba426d | ||
|
|
e9751b65d4 | ||
|
|
5abc2809b9 | ||
|
|
03f3a197cf | ||
|
|
6ed3f5cfd0 | ||
|
|
e372016826 | ||
|
|
fd699b67e9 | ||
|
|
827641193f | ||
|
|
fb7bd9c6de | ||
|
|
fcd995095d | ||
|
|
b7b5ec3711 | ||
|
|
1af642069c | ||
|
|
0343e6614b | ||
|
|
f1c0db247a | ||
|
|
61e838df46 | ||
|
|
057cbc26d9 | ||
|
|
22e8b9bc86 | ||
|
|
602b47f06a | ||
|
|
a086323a1f | ||
|
|
22140739ab | ||
|
|
55b71ce19e | ||
|
|
1a97c8bd86 | ||
|
|
b4e5f90afb | ||
|
|
2c1fc39f61 | ||
|
|
ee82c21718 | ||
|
|
10851cd3f4 | ||
|
|
a937ed2180 | ||
|
|
22be2f2dc0 | ||
|
|
833c0bc021 | ||
|
|
1251c1c8b3 | ||
|
|
5b428faea6 | ||
|
|
ca2dc5fae5 | ||
|
|
b9f6eff3d9 | ||
|
|
43c4d41877 | ||
|
|
609c6d3d64 | ||
|
|
8097935f5d | ||
|
|
4f7222f541 | ||
|
|
6bbb4d39bc | ||
|
|
a1f2c006b4 | ||
|
|
dfdadaa8ca | ||
|
|
856a32968b | ||
|
|
e4b83d2274 | ||
|
|
d06b468629 | ||
|
|
379058bc9d | ||
|
|
5534c8830c | ||
|
|
48a8937ff0 | ||
|
|
2dcccf0d4a | ||
|
|
bf266fb4da | ||
|
|
ffe49dd2fd | ||
|
|
ee16bdce6b | ||
|
|
44bae91f8e | ||
|
|
ae13eab23e | ||
|
|
f5f6ad4d4d | ||
|
|
0d6d481fcb | ||
|
|
99d9958e39 | ||
|
|
daae36b313 | ||
|
|
8556459f97 | ||
|
|
7f9e8ea7a3 | ||
|
|
01968fc637 | ||
|
|
04c2598cf4 | ||
|
|
77ded27994 | ||
|
|
ec6cf66bff |
No files matched your search
@@ -405,6 +405,10 @@ jobs:
|
||||
APPRUN
|
||||
sed -i "s/@APPIMAGE_ARCH@/$arch/" AppDir/AppRun
|
||||
chmod +x AppDir/AppRun
|
||||
# The AppImage runs as whoever starts it, not as this runner, so
|
||||
# everything in it must be readable by all. v12.69's FerretDB and
|
||||
# mongo-tools were 0711 and appimagetool refused to pack them.
|
||||
chmod -R a+rX AppDir
|
||||
|
||||
- name: Pack the AppImage
|
||||
if: ${{ env.BUILD_THIS == 'true' }}
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -97,6 +97,12 @@ jobs:
|
||||
|
||||
plan:
|
||||
needs: recover
|
||||
# The planner runs whatever recover's rerun ended with. A rerun that still
|
||||
# fails - Launchpad answering 503, a fault that is in the tagged code and
|
||||
# so in every rerun of it (v12.68's OpenBSD start) - used to skip the
|
||||
# planner and every build after it, so not even the assets that COULD be
|
||||
# filled were. recover stays red and names the jobs; the rest goes on.
|
||||
if: ${{ !cancelled() }}
|
||||
timeout-minutes: 15
|
||||
name: what the release is missing
|
||||
runs-on: ubuntu-latest
|
||||
@@ -211,7 +217,7 @@ jobs:
|
||||
contents: write
|
||||
name: ${{ matrix.arch }}
|
||||
needs: plan
|
||||
if: ${{ needs.plan.outputs.extra != '[]' }}
|
||||
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.extra != '[]' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 300
|
||||
strategy:
|
||||
@@ -331,7 +337,7 @@ jobs:
|
||||
bun:
|
||||
name: Missing Bun runtime bundles
|
||||
needs: plan
|
||||
if: ${{ needs.plan.outputs.bun != '[]' }}
|
||||
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.bun != '[]' }}
|
||||
uses: ./.github/workflows/bun.yml
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -345,7 +351,7 @@ jobs:
|
||||
appimage:
|
||||
name: AppImages
|
||||
needs: plan
|
||||
if: ${{ needs.plan.outputs.appimage != '[]' }}
|
||||
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.appimage != '[]' }}
|
||||
uses: ./.github/workflows/AppImage.yml
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -357,7 +363,7 @@ jobs:
|
||||
mac:
|
||||
name: Mac apps
|
||||
needs: plan
|
||||
if: ${{ needs.plan.outputs.mac != '[]' }}
|
||||
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.mac != '[]' }}
|
||||
uses: ./.github/workflows/mac.yml
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -369,7 +375,7 @@ jobs:
|
||||
flatpak:
|
||||
name: Flatpaks
|
||||
needs: plan
|
||||
if: ${{ needs.plan.outputs.flatpak != '[]' }}
|
||||
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.flatpak != '[]' }}
|
||||
uses: ./.github/workflows/Flatpak.yml
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -381,7 +387,7 @@ jobs:
|
||||
windows-exe:
|
||||
name: Windows single EXE
|
||||
needs: plan
|
||||
if: ${{ needs.plan.outputs.windows != '[]' }}
|
||||
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.windows != '[]' }}
|
||||
uses: ./.github/workflows/windows.yml
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -425,7 +431,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
name: Helm chart
|
||||
needs: plan
|
||||
if: ${{ !cancelled() && needs.plan.outputs.version != '' }}
|
||||
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.version != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout wekan (for releases/release-charts.sh)
|
||||
|
||||
@@ -38,7 +38,7 @@ run-name: Release v${{ inputs.new_version }}
|
||||
# only the ferretdb-<arch> binary it needs from the newest wekan/FerretDB
|
||||
# release (FerretDB is built + released separately by its own build.sh —
|
||||
# not compiled here) and embeds it
|
||||
# - release: writes the newest CHANGELOG.md notes to the GitHub Release and
|
||||
# - release: writes the newest CHANGELOG-big.md notes to the GitHub Release and
|
||||
# checks the core wekan-<version>-{amd64,arm64}.zip bundles are on
|
||||
# it. It uploads nothing: EVERY job that builds a release file
|
||||
# attaches it itself, as its last step, the moment it is built and
|
||||
@@ -281,8 +281,8 @@ jobs:
|
||||
# No `changelog:` output. The notes are 170 KB and more, which is past the
|
||||
# 128 KiB MAX_ARG_STRLEN a single environment variable may hold, so any job
|
||||
# consuming them through `env:` fails to start its shell. Jobs read
|
||||
# CHANGELOG.md from their own checkout instead - see
|
||||
# "Check the release notes exist in CHANGELOG.md" below.
|
||||
# CHANGELOG-big.md from their own checkout instead - see
|
||||
# "Check the release notes exist in CHANGELOG-big.md" below.
|
||||
steps:
|
||||
# Shallow: the only thing history was fetched for was finding out whether
|
||||
# the release tag already exists, which the tag step now asks GitHub.
|
||||
@@ -310,8 +310,8 @@ jobs:
|
||||
# string at MAX_ARG_STRLEN - 128 KiB - so execve refused to start bash at
|
||||
# all ("Argument list too long") before a line of the step ran. Each job
|
||||
# that needs the notes now extracts them from its own checkout of
|
||||
# CHANGELOG.md with the same script, straight into a file.
|
||||
- name: Check the release notes exist in CHANGELOG.md
|
||||
# CHANGELOG-big.md with the same script, straight into a file.
|
||||
- name: Check the release notes exist in CHANGELOG-big.md
|
||||
env:
|
||||
STEP_VERSION_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
@@ -1170,7 +1170,7 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
# ref: main, the same ref `prepare` read CHANGELOG.md from, so the notes
|
||||
# ref: main, the same ref `prepare` read CHANGELOG-big.md from, so the notes
|
||||
# composed below are the ones prepare validated. (bump pushes to main and
|
||||
# prepare tags it; nothing else pushes during a release.)
|
||||
#
|
||||
@@ -1203,7 +1203,7 @@ jobs:
|
||||
# published nothing, "Incorrect: command not found"); and `env:` put a
|
||||
# 172 KB string in envp, past the 128 KiB MAX_ARG_STRLEN, so execve
|
||||
# refused to start bash at all (v10.92, "Argument list too long"). Read
|
||||
# from CHANGELOG.md, appended with cat, the text is data all the way.
|
||||
# from CHANGELOG-big.md, appended with cat, the text is data all the way.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash releases/release-notes.sh "$VERSION" > release-notes.md
|
||||
@@ -3672,7 +3672,7 @@ jobs:
|
||||
# Ubuntu 24.04's AppArmor defaults kernel.apparmor_restrict_unprivileged_userns=1,
|
||||
# which blocks the unprivileged user namespaces that the Sandstorm install and
|
||||
# the spk supervisor rely on (the "fragile part" noted above). Relax it so
|
||||
# `install.sandstorm.io` and meteor-spk can create sandboxes on the runner.
|
||||
# `install.sandstorm.org` and meteor-spk can create sandboxes on the runner.
|
||||
# `|| true`: on kernels/runners without these knobs, setting them is a no-op.
|
||||
run: |
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
@@ -3696,11 +3696,11 @@ jobs:
|
||||
run: |
|
||||
# Downloaded, then run: a `curl | sh` cannot be retried, because by the
|
||||
# time the download fails sh is already half way through the script.
|
||||
bash "$GITHUB_WORKSPACE/releases/fetch.sh" -o /tmp/install-sandstorm.sh https://install.sandstorm.io
|
||||
bash "$GITHUB_WORKSPACE/releases/fetch.sh" -o /tmp/install-sandstorm.sh https://install.sandstorm.org
|
||||
sudo bash /tmp/install-sandstorm.sh -d
|
||||
|
||||
- name: Assemble modern meteor-spk.deps (Node 26 + FerretDB + Mongo 3.x CLIs)
|
||||
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.io (has mongod 3.0 +
|
||||
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.org (has mongod 3.0 +
|
||||
# niscud). build-deps.sh swaps in Node 26, adds FerretDB + the Mongo 3.x
|
||||
# CLIs + the migration launcher/importer, keeps niscud, and swaps in the
|
||||
# modern sandstorm-http-bridge from the Sandstorm install above. No dependency
|
||||
@@ -5061,11 +5061,20 @@ jobs:
|
||||
# infrastructure earns another one, up to this many, after a pause
|
||||
# that grows each time.
|
||||
max_attempts=6
|
||||
# A Launchpad build log is THIS attempt's when its name did not exist
|
||||
# before the attempt, or it was rewritten after the marker. The name
|
||||
# alone decides it where -nt compares whole seconds (bash 3.2): a log
|
||||
# written in the marker's second was not "newer", and was missed.
|
||||
logs_before=" "
|
||||
this_attempts_log() {
|
||||
[ -e "$1" ] || return 1
|
||||
case "$logs_before" in *" $1 "*) [ "$1" -nt "$attempt_marker" ] ;; *) return 0 ;; esac
|
||||
}
|
||||
infra_failure() { # did THIS attempt fail on Launchpad, not on WeKan?
|
||||
grep -qsE 'HTTP Error 5[0-9][0-9]|Error: Timeout|Service Unavailable' "$remote_log" && return 0
|
||||
local log
|
||||
for log in snapcraft-wekan-*.txt; do
|
||||
[ -e "$log" ] && [ "$log" -nt "$attempt_marker" ] || continue
|
||||
this_attempts_log "$log" || continue
|
||||
grep -qsE 'RPC failed;|GnuTLS recv error|fatal: early EOF|Gateway error \[IP: ' "$log" && return 0
|
||||
done
|
||||
return 1
|
||||
@@ -5090,6 +5099,7 @@ jobs:
|
||||
# earlier attempts' logs stay in the directory for the diagnostics
|
||||
# below, but must not be printed again as if they were this one's.
|
||||
touch "$attempt_marker"
|
||||
logs_before=" $(ls snapcraft-wekan-*.txt 2>/dev/null | tr '\n' ' ')"
|
||||
if [ "$fresh_next" = 1 ]; then
|
||||
echo "The previous attempt's Launchpad build finished without a snap; submitting a new build instead of recovering it."
|
||||
run_remote_build && rc=0 || rc=$?
|
||||
@@ -5146,7 +5156,7 @@ jobs:
|
||||
# and threw it away.
|
||||
new_logs=0
|
||||
for log in snapcraft-wekan-*.txt; do
|
||||
[ -e "$log" ] && [ "$log" -nt "$attempt_marker" ] || continue
|
||||
this_attempts_log "$log" || continue
|
||||
new_logs=1
|
||||
echo "::group::Launchpad build log: $log (what failed, then the last 150 lines)"
|
||||
# The tail alone is not where the cause is. snapcraft's own error is
|
||||
@@ -5684,7 +5694,7 @@ jobs:
|
||||
# rewrites the notes of a release that is already published.
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
# ref: main, so this job reads the same CHANGELOG.md the release job did.
|
||||
# ref: main, so this job reads the same CHANGELOG-big.md the release job did.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
@@ -32,6 +32,6 @@ jobs:
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Run chart-releaser
|
||||
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
|
||||
uses: helm/chart-releaser-action@df287104677278f869ca5480bd260358397cc3f9 # v1.8.0
|
||||
env:
|
||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -140,4 +140,7 @@ jobs:
|
||||
fi
|
||||
echo "Publishing $SPK with $SPK_BIN ..."
|
||||
# Same command as releases/release-sandstorm.sh: spk publish wekan-<VERSION>.spk
|
||||
"$SPK_BIN" publish "$SPK"
|
||||
# Publish to apps.sandstorm.org
|
||||
"$SPK_BIN" publish --webkey=https://api-b72c46193b266fd6c33e1299dccc9bfa.oasis.sandstorm.org#iCDu7VYl4qZxVbDLXLIE2XSU0bEadaBgApEMqgCkLmP "$SPK"
|
||||
# Publish to apps.sandstorm.io
|
||||
"$SPK_BIN" publish --webkey=https://alpha-api.sandstorm.io/#Rs-0TT13YrNSbv7Fiz5K9bBkLaJn3E5TB0PU1GSn1HE "$SPK"
|
||||
@@ -35,7 +35,7 @@ contributor — that offer is the mistake, not the rule.
|
||||
The maintainer's half is the release flow, not taste. Releases here are frequent —
|
||||
several a day when a fault is being chased — and `releases/release-all.sh` cuts one
|
||||
from whatever is on `main`, taking the version from the `# Upcoming WeKan ® release`
|
||||
section of `CHANGELOG.md`. Work parked on a branch is work that is not in the next
|
||||
section of `CHANGELOG-big.md`. Work parked on a branch is work that is not in the next
|
||||
release, and `tests/changelogEntriesBelongToTheirRelease.test.cjs` fails when a
|
||||
released section links a commit that release does not contain.
|
||||
|
||||
@@ -340,7 +340,7 @@ directly after the merge.
|
||||
the two patch repositories under `.tools/` — node-patches and mongo-tools-patches.
|
||||
- Directory structure:
|
||||
- `wekan` — this repo (https://github.com/wekan/wekan); see
|
||||
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG.md` at root. It is at
|
||||
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG-big.md` and `CHANGELOG.md` at root. It is at
|
||||
`~/repos/wekan` on Linux, `~/Documents/repos/wekan` on macOS and
|
||||
`Downloads\repos\wekan` on Windows. The maintainer commits on `main`; a
|
||||
contributor works on a branch and opens a pull request — see "Where this
|
||||
@@ -414,7 +414,7 @@ directly after the merge.
|
||||
|
||||
| Repository | File | Format |
|
||||
| --- | --- | --- |
|
||||
| `wekan/wekan` | `CHANGELOG.md` | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
|
||||
| `wekan/wekan` | `CHANGELOG-big.md` (and the generated `CHANGELOG.md`, below) | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
|
||||
| `wekan/node-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming node-patches release` |
|
||||
| `wekan/mongo-tools-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming mongo-tools-patches release` |
|
||||
| `wekan/FerretDB` | `CHANGELOG.md` | **upstream FerretDB's** format, not WeKan's: `## [v1.48.0](tag URL) (YYYY-MM-DD)` and `### New Features 🎉` / `### Fixed 🐛` / `### Other Changes 🤖` bullets ending `by @xet7. Thanks to xet7.` |
|
||||
@@ -426,6 +426,52 @@ directly after the merge.
|
||||
out and would break the tooling that parses them. When this file and the file being
|
||||
edited disagree, **the file being edited wins** — and everything below in this
|
||||
section is about the WeKan format specifically.
|
||||
- **WeKan has TWO changelogs, and only one is written.** `CHANGELOG-big.md` is
|
||||
the changelog: `# Status`, `# Upcoming WeKan ® release` and every release in
|
||||
full, in the `<details>` format below - every rule in the rest of this section
|
||||
is about it, and every entry, fix and release goes into it. `CHANGELOG.md` is
|
||||
the SHORT changelog, generated from it by `node releases/changelog-short.mjs`
|
||||
and never edited by hand:
|
||||
|
||||
| | `CHANGELOG.md` |
|
||||
| --- | --- |
|
||||
| Status, TODO Later | none |
|
||||
| First | the unreleased `# Upcoming WeKan ® release`, when there is one: its **In short** paragraph only, then `[All changes so far](CHANGELOG-big.md#upcoming-wekan--release)` |
|
||||
| Below it | the five newest `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` |
|
||||
| Per release | its **In short** paragraph only, then `[All changes in v…](CHANGELOG-big.md#anchor)` |
|
||||
| Markup | plain Markdown: no `<details>`, `<summary>`, `</summary>` or `</details>`, and every HTML link a Markdown link |
|
||||
|
||||
It exists because the big file is for whoever writes and audits a release,
|
||||
and most readers - GitHub's front page, the Sandstorm App Market
|
||||
(`sandstorm-pkgdef.capnp` embeds it), the Helm chart's changelog link - want
|
||||
what changed lately, at a glance. `releases/release-all.sh` regenerates it
|
||||
right after naming the new release, so it is in the same release commit, and
|
||||
`releases/changelog.sh` does the same; `tests/changelogShort.test.cjs` fails
|
||||
when it is not exactly what the generator writes. So: write
|
||||
`CHANGELOG-big.md`, run the generator, commit both - and never edit
|
||||
`CHANGELOG.md` to fix something, because the next run overwrites it.
|
||||
- **"Update wekan changelog" means both files, in this order:**
|
||||
1. Find what is not in the changelog yet: `git log` since the newest commit
|
||||
`CHANGELOG-big.md`'s Upcoming section (or, when it has none, the newest
|
||||
release's `Prepare v… release` commit) already covers, by anybody - a
|
||||
merged pull request and the maintainer's own commits alike.
|
||||
2. Check whether a release was cut since the last update (`release-all.sh`
|
||||
renames Upcoming to `# v<NEW> …` and opens no new one). If there is no
|
||||
`# Upcoming WeKan ® release` heading, add it from
|
||||
[docs/DeveloperDocs/Changelog-Upcoming-Template.md](docs/DeveloperDocs/Changelog-Upcoming-Template.md)
|
||||
above the newest release - never add an entry to a released section.
|
||||
3. Write the entries into that Upcoming section of `CHANGELOG-big.md`, in the
|
||||
format described below (subsections, topic groups, `<details>` entries
|
||||
linking each commit), and update its **In short** paragraph so it still
|
||||
names the release's main outcomes.
|
||||
4. Run `node releases/changelog-short.mjs`: `CHANGELOG.md` then shows that
|
||||
Upcoming **In short** first and the five newest releases below it, with the
|
||||
longer details only in `CHANGELOG-big.md`.
|
||||
5. Run the changelog tests - `tests/changelogShort.test.cjs`,
|
||||
`tests/changelogFormat.test.cjs`,
|
||||
`tests/changelogEntriesBelongToTheirRelease.test.cjs` (or
|
||||
`node tests/run-node-suites.cjs changelog`) - and commit
|
||||
`CHANGELOG-big.md` and `CHANGELOG.md` together.
|
||||
- During development, add entries under a new `# Upcoming WeKan ® release` section above
|
||||
the newest release (FerretDB uses `## Upcoming FerretDB release`; the patch repos use
|
||||
`# Upcoming <repo> release`). Do **not** hand-edit
|
||||
@@ -438,10 +484,10 @@ directly after the merge.
|
||||
add and syntax-check/register the test and state clearly what could not be run.
|
||||
Do not duplicate an existing test merely to satisfy the audit — extend it when
|
||||
that keeps one behavior in one suite.
|
||||
- **CHANGELOG.md stays small enough for GitHub to show it.** GitHub renders
|
||||
- **CHANGELOG-big.md stays small enough for GitHub to show it.** GitHub renders
|
||||
Markdown up to 500 KiB; above that it shows only plain source text, and above
|
||||
about 2 MB nothing at all ("we can't show files that are this big right
|
||||
now"). CHANGELOG.md reached 2.6 MB over 1,100 releases back to 2015 (#6580),
|
||||
now"). The changelog reached 2.6 MB over 1,100 releases back to 2015 (#6580),
|
||||
and keeping only the current month still came to 2.1 MB, because releases
|
||||
here are frequent and large. So the cut is by SIZE: 450,000 bytes
|
||||
(`BUDGET` in `releases/changelog-archive.mjs`), under GitHub's limit with
|
||||
@@ -449,7 +495,7 @@ directly after the merge.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `CHANGELOG.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
|
||||
| `CHANGELOG-big.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
|
||||
| `old-CHANGELOG/<year>/<MM>.md` | a month's releases, newest first; a month over the budget continues in `<MM>-part2.md`, `<MM>-part3.md`, ... |
|
||||
| `old-CHANGELOG/<year>.md` | 2015–2025, whole |
|
||||
|
||||
@@ -464,11 +510,11 @@ directly after the merge.
|
||||
still in git (`gitk`, `git-gui`, `git log --follow`), and being small enough
|
||||
to open is worth more.
|
||||
|
||||
Run `node releases/changelog-archive.mjs` whenever CHANGELOG.md has grown -
|
||||
Run `node releases/changelog-archive.mjs` whenever CHANGELOG-big.md has grown -
|
||||
after a release is the natural moment. It is idempotent — a run with nothing
|
||||
to move only refreshes the tables and parts — and it decides from the FILE
|
||||
rather than the clock, so two people running it agree.
|
||||
`tests/changelogArchive.test.cjs` fails when CHANGELOG.md is over the budget.
|
||||
`tests/changelogArchive.test.cjs` fails when CHANGELOG-big.md is over the budget.
|
||||
An archived section is never edited, for the same reason a released one is
|
||||
not.
|
||||
- **The file's shape, top to bottom** — keep it exactly as it is now. There is
|
||||
@@ -564,7 +610,7 @@ directly after the merge.
|
||||
`<details>` blocks below carry that information. Keep the paragraph current as
|
||||
topics change, and shorten it when added commits make it grow. A finished release
|
||||
keeps the paragraph it was written with.
|
||||
- **Neither CHANGELOG.md nor release notes may contain a binaries provenance
|
||||
- **Neither changelog nor the release notes may contain a binaries provenance
|
||||
section or a Platform/Bundle/Binary/From/Version/Checked/SHA256 table.**
|
||||
Do not add "Binaries in these bundles" at the top or bottom of either.
|
||||
Release notes summarize each Translations group as only a list of updated
|
||||
@@ -577,7 +623,7 @@ directly after the merge.
|
||||
no security fixes - is left out rather than added empty.
|
||||
Release notes have only In short, Security, Translations (language list),
|
||||
the standard thanks line and More details at ChangeLog with the release
|
||||
anchor link. Other details stay in CHANGELOG.md. Keep download
|
||||
anchor link into CHANGELOG-big.md. Other details stay in CHANGELOG-big.md. Keep download
|
||||
verification and `provenance.tsv` build artifacts for diagnostics; do not
|
||||
append their contents or `releases/provenance-table.sh` output to notes.
|
||||
- **Inside a subsection, entries are GROUPED BY TOPIC/AREA.** A release touches a handful
|
||||
@@ -636,7 +682,7 @@ directly after the merge.
|
||||
Because CRITICAL comes first, it keeps the `This release ` prefix; a following non-security
|
||||
subsection becomes `and …` per the rule above.
|
||||
- **TODO Later** — a `<details>` nested inside `# Status` near the TOP of
|
||||
`CHANGELOG.md` (above the version sections), a triage backlog for open
|
||||
`CHANGELOG-big.md` (above the version sections), a triage backlog for open
|
||||
issues that were **investigated but not fixed here**, each
|
||||
recorded with a concrete REASON so whoever picks it up next knows why. Use it when working
|
||||
through open issues (the "Fix open issues" process): for each issue, either **fix it** (commit
|
||||
@@ -717,7 +763,7 @@ have cost a released section its accuracy:
|
||||
- **Work continues immediately after a release**, so an entry written right
|
||||
after one has to go somewhere. `release-all.sh` renames `# Upcoming WeKan ®
|
||||
release` to `# v<NEW> …` and stops there — it does **not** create a new,
|
||||
empty `# Upcoming` section. CHANGELOG.md never carries an
|
||||
empty `# Upcoming` section. CHANGELOG-big.md never carries an
|
||||
`**In short:** nothing here yet.` placeholder sitting between releases; it
|
||||
used to (via a now-deleted `releases/changelog-open-next.mjs`), and that
|
||||
meant the file always had a section saying nothing, from the moment a
|
||||
@@ -869,6 +915,21 @@ have cost a released section its accuracy:
|
||||
sizes them at 24px, and keeps the icon and its red drop on one line at every width
|
||||
(they are one icon) — the table itself stays percentage-sized with no fixed or
|
||||
minimum width, so it fills the page at every browser width.
|
||||
- **A report that arrives WITHOUT a GHSA is upgraded when its GHSA arrives.** The
|
||||
stars follow `.tools/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
|
||||
suggested fix is 5, a report sent to security@wekan.fi or any other way, with a
|
||||
suggested fix, is 3. When that researcher is later able to add a GitHub security
|
||||
advisory for the same report, the maintainer requests the CVE through the GHSA
|
||||
process, and the row's 3 stars become 5. So whenever an AI updates or audits the
|
||||
Hall of Fame, it checks each 3-star row whose Process says the report came
|
||||
without a GHSA against [WeKan Security
|
||||
Advisories](https://github.com/wekan/wekan/security/advisories): when a GHSA for
|
||||
that report now exists, it sets the row to 5 stars on the index AND on the row's
|
||||
own page, links the GHSA (and the CVE in the CVE cell once one is assigned),
|
||||
rewrites the Process cell to say the report later arrived as a GHSA, and records
|
||||
the change in Audit-Stars.md. Match by the report's subject and reporter, not by
|
||||
a nearby advisory - a GHSA about something else is no upgrade. Until the GHSA
|
||||
exists, the row stays at 3 and its Process cell says the GHSA is awaited.
|
||||
- **On the Hall of Fame index page, the Process and Vulnerabilities cells are
|
||||
collapsed.** Those two columns carry the prose — how the report arrived, and the
|
||||
whole story of the vulnerability — so the page would be one wall of text and the
|
||||
|
||||
+6927
File diff suppressed because it is too large.
Load diff
+37
-5476
File diff suppressed because it is too large.
Load diff
@@ -35,7 +35,7 @@ contributor — that offer is the mistake, not the rule.
|
||||
The maintainer's half is the release flow, not taste. Releases here are frequent —
|
||||
several a day when a fault is being chased — and `releases/release-all.sh` cuts one
|
||||
from whatever is on `main`, taking the version from the `# Upcoming WeKan ® release`
|
||||
section of `CHANGELOG.md`. Work parked on a branch is work that is not in the next
|
||||
section of `CHANGELOG-big.md`. Work parked on a branch is work that is not in the next
|
||||
release, and `tests/changelogEntriesBelongToTheirRelease.test.cjs` fails when a
|
||||
released section links a commit that release does not contain.
|
||||
|
||||
@@ -340,7 +340,7 @@ directly after the merge.
|
||||
the two patch repositories under `.tools/` — node-patches and mongo-tools-patches.
|
||||
- Directory structure:
|
||||
- `wekan` — this repo (https://github.com/wekan/wekan); see
|
||||
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG.md` at root. It is at
|
||||
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG-big.md` and `CHANGELOG.md` at root. It is at
|
||||
`~/repos/wekan` on Linux, `~/Documents/repos/wekan` on macOS and
|
||||
`Downloads\repos\wekan` on Windows. The maintainer commits on `main`; a
|
||||
contributor works on a branch and opens a pull request — see "Where this
|
||||
@@ -414,7 +414,7 @@ directly after the merge.
|
||||
|
||||
| Repository | File | Format |
|
||||
| --- | --- | --- |
|
||||
| `wekan/wekan` | `CHANGELOG.md` | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
|
||||
| `wekan/wekan` | `CHANGELOG-big.md` (and the generated `CHANGELOG.md`, below) | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
|
||||
| `wekan/node-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming node-patches release` |
|
||||
| `wekan/mongo-tools-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming mongo-tools-patches release` |
|
||||
| `wekan/FerretDB` | `CHANGELOG.md` | **upstream FerretDB's** format, not WeKan's: `## [v1.48.0](tag URL) (YYYY-MM-DD)` and `### New Features 🎉` / `### Fixed 🐛` / `### Other Changes 🤖` bullets ending `by @xet7. Thanks to xet7.` |
|
||||
@@ -426,6 +426,52 @@ directly after the merge.
|
||||
out and would break the tooling that parses them. When this file and the file being
|
||||
edited disagree, **the file being edited wins** — and everything below in this
|
||||
section is about the WeKan format specifically.
|
||||
- **WeKan has TWO changelogs, and only one is written.** `CHANGELOG-big.md` is
|
||||
the changelog: `# Status`, `# Upcoming WeKan ® release` and every release in
|
||||
full, in the `<details>` format below - every rule in the rest of this section
|
||||
is about it, and every entry, fix and release goes into it. `CHANGELOG.md` is
|
||||
the SHORT changelog, generated from it by `node releases/changelog-short.mjs`
|
||||
and never edited by hand:
|
||||
|
||||
| | `CHANGELOG.md` |
|
||||
| --- | --- |
|
||||
| Status, TODO Later | none |
|
||||
| First | the unreleased `# Upcoming WeKan ® release`, when there is one: its **In short** paragraph only, then `[All changes so far](CHANGELOG-big.md#upcoming-wekan--release)` |
|
||||
| Below it | the five newest `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` |
|
||||
| Per release | its **In short** paragraph only, then `[All changes in v…](CHANGELOG-big.md#anchor)` |
|
||||
| Markup | plain Markdown: no `<details>`, `<summary>`, `</summary>` or `</details>`, and every HTML link a Markdown link |
|
||||
|
||||
It exists because the big file is for whoever writes and audits a release,
|
||||
and most readers - GitHub's front page, the Sandstorm App Market
|
||||
(`sandstorm-pkgdef.capnp` embeds it), the Helm chart's changelog link - want
|
||||
what changed lately, at a glance. `releases/release-all.sh` regenerates it
|
||||
right after naming the new release, so it is in the same release commit, and
|
||||
`releases/changelog.sh` does the same; `tests/changelogShort.test.cjs` fails
|
||||
when it is not exactly what the generator writes. So: write
|
||||
`CHANGELOG-big.md`, run the generator, commit both - and never edit
|
||||
`CHANGELOG.md` to fix something, because the next run overwrites it.
|
||||
- **"Update wekan changelog" means both files, in this order:**
|
||||
1. Find what is not in the changelog yet: `git log` since the newest commit
|
||||
`CHANGELOG-big.md`'s Upcoming section (or, when it has none, the newest
|
||||
release's `Prepare v… release` commit) already covers, by anybody - a
|
||||
merged pull request and the maintainer's own commits alike.
|
||||
2. Check whether a release was cut since the last update (`release-all.sh`
|
||||
renames Upcoming to `# v<NEW> …` and opens no new one). If there is no
|
||||
`# Upcoming WeKan ® release` heading, add it from
|
||||
[docs/DeveloperDocs/Changelog-Upcoming-Template.md](docs/DeveloperDocs/Changelog-Upcoming-Template.md)
|
||||
above the newest release - never add an entry to a released section.
|
||||
3. Write the entries into that Upcoming section of `CHANGELOG-big.md`, in the
|
||||
format described below (subsections, topic groups, `<details>` entries
|
||||
linking each commit), and update its **In short** paragraph so it still
|
||||
names the release's main outcomes.
|
||||
4. Run `node releases/changelog-short.mjs`: `CHANGELOG.md` then shows that
|
||||
Upcoming **In short** first and the five newest releases below it, with the
|
||||
longer details only in `CHANGELOG-big.md`.
|
||||
5. Run the changelog tests - `tests/changelogShort.test.cjs`,
|
||||
`tests/changelogFormat.test.cjs`,
|
||||
`tests/changelogEntriesBelongToTheirRelease.test.cjs` (or
|
||||
`node tests/run-node-suites.cjs changelog`) - and commit
|
||||
`CHANGELOG-big.md` and `CHANGELOG.md` together.
|
||||
- During development, add entries under a new `# Upcoming WeKan ® release` section above
|
||||
the newest release (FerretDB uses `## Upcoming FerretDB release`; the patch repos use
|
||||
`# Upcoming <repo> release`). Do **not** hand-edit
|
||||
@@ -438,10 +484,10 @@ directly after the merge.
|
||||
add and syntax-check/register the test and state clearly what could not be run.
|
||||
Do not duplicate an existing test merely to satisfy the audit — extend it when
|
||||
that keeps one behavior in one suite.
|
||||
- **CHANGELOG.md stays small enough for GitHub to show it.** GitHub renders
|
||||
- **CHANGELOG-big.md stays small enough for GitHub to show it.** GitHub renders
|
||||
Markdown up to 500 KiB; above that it shows only plain source text, and above
|
||||
about 2 MB nothing at all ("we can't show files that are this big right
|
||||
now"). CHANGELOG.md reached 2.6 MB over 1,100 releases back to 2015 (#6580),
|
||||
now"). The changelog reached 2.6 MB over 1,100 releases back to 2015 (#6580),
|
||||
and keeping only the current month still came to 2.1 MB, because releases
|
||||
here are frequent and large. So the cut is by SIZE: 450,000 bytes
|
||||
(`BUDGET` in `releases/changelog-archive.mjs`), under GitHub's limit with
|
||||
@@ -449,7 +495,7 @@ directly after the merge.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `CHANGELOG.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
|
||||
| `CHANGELOG-big.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
|
||||
| `old-CHANGELOG/<year>/<MM>.md` | a month's releases, newest first; a month over the budget continues in `<MM>-part2.md`, `<MM>-part3.md`, ... |
|
||||
| `old-CHANGELOG/<year>.md` | 2015–2025, whole |
|
||||
|
||||
@@ -464,11 +510,11 @@ directly after the merge.
|
||||
still in git (`gitk`, `git-gui`, `git log --follow`), and being small enough
|
||||
to open is worth more.
|
||||
|
||||
Run `node releases/changelog-archive.mjs` whenever CHANGELOG.md has grown -
|
||||
Run `node releases/changelog-archive.mjs` whenever CHANGELOG-big.md has grown -
|
||||
after a release is the natural moment. It is idempotent — a run with nothing
|
||||
to move only refreshes the tables and parts — and it decides from the FILE
|
||||
rather than the clock, so two people running it agree.
|
||||
`tests/changelogArchive.test.cjs` fails when CHANGELOG.md is over the budget.
|
||||
`tests/changelogArchive.test.cjs` fails when CHANGELOG-big.md is over the budget.
|
||||
An archived section is never edited, for the same reason a released one is
|
||||
not.
|
||||
- **The file's shape, top to bottom** — keep it exactly as it is now. There is
|
||||
@@ -564,7 +610,7 @@ directly after the merge.
|
||||
`<details>` blocks below carry that information. Keep the paragraph current as
|
||||
topics change, and shorten it when added commits make it grow. A finished release
|
||||
keeps the paragraph it was written with.
|
||||
- **Neither CHANGELOG.md nor release notes may contain a binaries provenance
|
||||
- **Neither changelog nor the release notes may contain a binaries provenance
|
||||
section or a Platform/Bundle/Binary/From/Version/Checked/SHA256 table.**
|
||||
Do not add "Binaries in these bundles" at the top or bottom of either.
|
||||
Release notes summarize each Translations group as only a list of updated
|
||||
@@ -577,7 +623,7 @@ directly after the merge.
|
||||
no security fixes - is left out rather than added empty.
|
||||
Release notes have only In short, Security, Translations (language list),
|
||||
the standard thanks line and More details at ChangeLog with the release
|
||||
anchor link. Other details stay in CHANGELOG.md. Keep download
|
||||
anchor link into CHANGELOG-big.md. Other details stay in CHANGELOG-big.md. Keep download
|
||||
verification and `provenance.tsv` build artifacts for diagnostics; do not
|
||||
append their contents or `releases/provenance-table.sh` output to notes.
|
||||
- **Inside a subsection, entries are GROUPED BY TOPIC/AREA.** A release touches a handful
|
||||
@@ -636,7 +682,7 @@ directly after the merge.
|
||||
Because CRITICAL comes first, it keeps the `This release ` prefix; a following non-security
|
||||
subsection becomes `and …` per the rule above.
|
||||
- **TODO Later** — a `<details>` nested inside `# Status` near the TOP of
|
||||
`CHANGELOG.md` (above the version sections), a triage backlog for open
|
||||
`CHANGELOG-big.md` (above the version sections), a triage backlog for open
|
||||
issues that were **investigated but not fixed here**, each
|
||||
recorded with a concrete REASON so whoever picks it up next knows why. Use it when working
|
||||
through open issues (the "Fix open issues" process): for each issue, either **fix it** (commit
|
||||
@@ -717,7 +763,7 @@ have cost a released section its accuracy:
|
||||
- **Work continues immediately after a release**, so an entry written right
|
||||
after one has to go somewhere. `release-all.sh` renames `# Upcoming WeKan ®
|
||||
release` to `# v<NEW> …` and stops there — it does **not** create a new,
|
||||
empty `# Upcoming` section. CHANGELOG.md never carries an
|
||||
empty `# Upcoming` section. CHANGELOG-big.md never carries an
|
||||
`**In short:** nothing here yet.` placeholder sitting between releases; it
|
||||
used to (via a now-deleted `releases/changelog-open-next.mjs`), and that
|
||||
meant the file always had a section saying nothing, from the moment a
|
||||
@@ -869,6 +915,21 @@ have cost a released section its accuracy:
|
||||
sizes them at 24px, and keeps the icon and its red drop on one line at every width
|
||||
(they are one icon) — the table itself stays percentage-sized with no fixed or
|
||||
minimum width, so it fills the page at every browser width.
|
||||
- **A report that arrives WITHOUT a GHSA is upgraded when its GHSA arrives.** The
|
||||
stars follow `.tools/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
|
||||
suggested fix is 5, a report sent to security@wekan.fi or any other way, with a
|
||||
suggested fix, is 3. When that researcher is later able to add a GitHub security
|
||||
advisory for the same report, the maintainer requests the CVE through the GHSA
|
||||
process, and the row's 3 stars become 5. So whenever an AI updates or audits the
|
||||
Hall of Fame, it checks each 3-star row whose Process says the report came
|
||||
without a GHSA against [WeKan Security
|
||||
Advisories](https://github.com/wekan/wekan/security/advisories): when a GHSA for
|
||||
that report now exists, it sets the row to 5 stars on the index AND on the row's
|
||||
own page, links the GHSA (and the CVE in the CVE cell once one is assigned),
|
||||
rewrites the Process cell to say the report later arrived as a GHSA, and records
|
||||
the change in Audit-Stars.md. Match by the report's subject and reporter, not by
|
||||
a nearby advisory - a GHSA about something else is no upgrade. Until the GHSA
|
||||
exists, the row stays at 3 and its Process cell says the GHSA is awaited.
|
||||
- **On the Hall of Fame index page, the Process and Vulnerabilities cells are
|
||||
collapsed.** Those two columns carry the prose — how the report arrived, and the
|
||||
whole story of the vulnerability — so the page would be one wall of text and the
|
||||
|
||||
+9
-1
@@ -38,7 +38,7 @@ FROM --platform=$BUILDPLATFORM debian:trixie AS bundle
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=12.68
|
||||
ARG VERSION=12.78
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
COPY --chmod=755 releases/check-telemetry.py /tmp/check-telemetry.py
|
||||
@@ -292,6 +292,14 @@ ENV \
|
||||
LOGOUT_ON_HOURS="" \
|
||||
LOGOUT_ON_MINUTES="" \
|
||||
USERS_DISABLE_INACTIVE_AFTER_DAYS="" \
|
||||
LOGOUT_IDLE_MINUTES="" \
|
||||
REQUIRE_2FA="" \
|
||||
PASSWORD_MIN_LENGTH="" \
|
||||
PASSWORD_MIN_LENGTH_ENFORCE="" \
|
||||
PASSWORD_CHECK_PWNED="" \
|
||||
HSTS_MAX_AGE="" \
|
||||
HSTS_INCLUDE_SUBDOMAINS="" \
|
||||
HSTS_PRELOAD="" \
|
||||
ATTACHMENTS_RETENTION_DAYS="" \
|
||||
ATTACHMENTS_RETENTION_MODE="" \
|
||||
ATTACHMENTS_PURGE_DELETED_AFTER_DAYS="" \
|
||||
|
||||
+13
-5
@@ -29,7 +29,15 @@ because WeKan is NOT Big Tech. WeKan is FLOSS.
|
||||
- **Remediation**: Please wait for a new WeKan release that addresses the issue.
|
||||
Fixes are announced at the top of the [ChangeLog](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
|
||||
- **Recognition**: We will acknowledge your contribution by adding you to our
|
||||
[Hall of Fame](https://wekan.fi/hall-of-fame/).
|
||||
[Hall of Fame](https://wekan.fi/hall-of-fame/). The stars follow the
|
||||
[Stars Audit](https://github.com/wekan/wekan.fi/blob/main/hall-of-fame/Audit-Stars.md):
|
||||
a GitHub security advisory (GHSA) with details and a suggested fix gets 5 stars,
|
||||
a report sent to security@wekan.fi or some other way, with a suggested fix, gets 3.
|
||||
- **Adding the GHSA later**: if you sent your report to security@wekan.fi, or some
|
||||
other way, and are later able to add it as a "New draft security advisory" at
|
||||
[WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories),
|
||||
please do. The maintainer of WeKan then requests the CVE through that GHSA, and
|
||||
your Hall of Fame row is upgraded from 3 stars to 5.
|
||||
- **CVE Policy**: WeKan Security requests CVE at GitHub when releasing Security Advisory.
|
||||
|
||||
### 3. Post-Release and Public Disclosure:
|
||||
@@ -264,13 +272,13 @@ Meteor.startup(() => {
|
||||
|
||||
On the Sandstorm platform, Standalone WeKan features like the Admin Panel are turned off using environment variables, because Sandstorm platform provides SSO for all apps running on Sandstorm.
|
||||
|
||||
[Sandstorm](https://sandstorm.io) is a separate Open Source platform that has been
|
||||
[security audited](https://sandstorm.io/news/2017-03-02-security-review) and found bugs fixed.
|
||||
[Sandstorm](https://sandstorm.org) is a separate Open Source platform that has been
|
||||
[security audited](https://sandstorm.org/news/2017-03-02-security-review) and found bugs fixed.
|
||||
Sandstorm also has passwordless login, LDAP, SAML, Google, and other auth options already.
|
||||
On Sandstorm, code is read-only and signed by app maintainers; only grain content can be modified.
|
||||
WeKan on Sandstorm runs in a sandboxed grain; it does not have access elsewhere without a user-visible PowerBox request or opening a randomly-generated API key URL.
|
||||
Also read [Sandstorm Security Practices](https://docs.sandstorm.io/en/latest/using/security-practices/) and
|
||||
[Sandstorm Security non-events](https://docs.sandstorm.io/en/latest/using/security-non-events/).
|
||||
Also read [Sandstorm Security Practices](https://docs.sandstorm.org/en/latest/using/security-practices/) and
|
||||
[Sandstorm Security non-events](https://docs.sandstorm.org/en/latest/using/security-non-events/).
|
||||
For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email.
|
||||
|
||||
## What WeKan bugs are eligible?
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v12.68.0"
|
||||
appVersion: "v12.78.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -1282,7 +1282,7 @@ if arguments >= 1:
|
||||
|
||||
# ------- BOARD MEMBER / CARD FIELD API ENDPOINTS START -----------
|
||||
# These commands wrap REST endpoints that already exist in Wekan.
|
||||
# See CHANGELOG.md "Upcoming" section for related issues and design notes.
|
||||
# See CHANGELOG-big.md "Upcoming" section for related issues and design notes.
|
||||
|
||||
# Map a human readable ROLE to the board member permission boolean flags
|
||||
# used by /api/boards/:boardId/members/:userId/add and .../members/:memberId
|
||||
@@ -1472,7 +1472,7 @@ if arguments >= 1:
|
||||
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
# NOTE: this REPLACES the card's labels. Bulk add/remove (merge) is not yet
|
||||
# implemented server-side, see Issue #5819 in CHANGELOG.md "Upcoming".
|
||||
# implemented server-side, see Issue #5819 in CHANGELOG-big.md "Upcoming".
|
||||
put_data = {'labelIds': labelids if labelids else []}
|
||||
body = requests.put(edcard, json=put_data, headers=headers)
|
||||
print("=== SET CARD LABELS ===\n")
|
||||
|
||||
@@ -989,15 +989,18 @@ exit /b %errorlevel%
|
||||
|
||||
:buildcommon
|
||||
REM The same diagnostics as build.sh build_wekan: npm verbose output and
|
||||
REM foreground install scripts, METEOR_PROFILE, Node module resolution and the
|
||||
REM resolver command/output tracer, so a failed Windows build leaves the same
|
||||
REM evidence in its log. WEKAN_BUILD_HEAP_SNAPSHOT=1 caps the heap at
|
||||
REM foreground install scripts, METEOR_PROFILE and the resolver command/output
|
||||
REM tracer, so a failed Windows build leaves the same evidence in its log. Node
|
||||
REM module resolution (one line per require, gigabytes a minute) only with
|
||||
REM WEKAN_BUILD_TRACE_MODULES=1. WEKAN_BUILD_HEAP_SNAPSHOT=1 caps the heap at
|
||||
REM WEKAN_BUILD_HEAP_SNAPSHOT_MB (4096) and writes a heap snapshot near the limit.
|
||||
setlocal
|
||||
set "npm_config_loglevel=verbose"
|
||||
set "npm_config_foreground_scripts=true"
|
||||
if not defined METEOR_PROFILE set "METEOR_PROFILE=100"
|
||||
if defined NODE_DEBUG (set "NODE_DEBUG=%NODE_DEBUG%,module") else (set "NODE_DEBUG=module")
|
||||
if "%WEKAN_BUILD_TRACE_MODULES%"=="1" (
|
||||
if defined NODE_DEBUG (set "NODE_DEBUG=!NODE_DEBUG!,module") else (set "NODE_DEBUG=module")
|
||||
)
|
||||
if "%WEKAN_BUILD_HEAP_SNAPSHOT%"=="1" (
|
||||
if not defined WEKAN_BUILD_HEAP_SNAPSHOT_MB set "WEKAN_BUILD_HEAP_SNAPSHOT_MB=4096"
|
||||
set "TOOL_NODE_FLAGS=--max-old-space-size=!WEKAN_BUILD_HEAP_SNAPSHOT_MB! --heapsnapshot-near-heap-limit=1"
|
||||
@@ -1005,7 +1008,7 @@ if "%WEKAN_BUILD_HEAP_SNAPSHOT%"=="1" (
|
||||
echo Heap snapshot: ON, heap capped at !WEKAN_BUILD_HEAP_SNAPSHOT_MB! MB; the build will fail sooner and write Heap.*.heapsnapshot.
|
||||
)
|
||||
set "NODE_OPTIONS=%NODE_OPTIONS% --require="%REPO%\tools\build-command-output.cjs""
|
||||
echo Build diagnostics: resolver command/output tracing, Node module resolution, npm verbose output, foreground install scripts, METEOR_PROFILE=%METEOR_PROFILE% ^(milliseconds^).
|
||||
echo Build diagnostics: resolver command/output tracing, npm verbose output, foreground install scripts, METEOR_PROFILE=%METEOR_PROFILE% ^(milliseconds^).
|
||||
call :buildcommon_steps
|
||||
set "BUILD_RC=%errorlevel%"
|
||||
if not "%BUILD_RC%"=="0" if defined WEKAN_BUILD_LOG (
|
||||
@@ -1977,7 +1980,7 @@ netstat -ano | findstr /r /c:":3001 .*LISTENING" >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
echo ==^> Reusing the MongoDB already listening on :3001 ^(not started or stopped by this run^).
|
||||
) else (
|
||||
for %%i in ("%REPO%\..\mongodb-test-3001") do set "DBPATH=%%~fi"
|
||||
for %%i in ("%REPO%\.tools\mongodb-test-3001") do set "DBPATH=%%~fi"
|
||||
if not exist "!DBPATH!" md "!DBPATH!"
|
||||
echo ==^> Starting MongoDB ^(Meteor's mongod^) on :3001, dbpath !DBPATH!.
|
||||
start "WekanTestMongo" /MIN "%MONGOD_BIN%" --port 3001 --dbpath "!DBPATH!" --bind_ip 127.0.0.1 --logpath "%RUN_LOGDIR%\wekan-test-mongod.log"
|
||||
|
||||
@@ -468,10 +468,17 @@ function build_wekan(){
|
||||
echo "===== wekan build started $(date '+%F %T') ====="
|
||||
export npm_config_loglevel=verbose npm_config_foreground_scripts=true
|
||||
export METEOR_PROFILE="${METEOR_PROFILE:-100}"
|
||||
export NODE_DEBUG="${NODE_DEBUG:+$NODE_DEBUG,}module"
|
||||
# Node module resolution, one line per require. OFF unless
|
||||
# WEKAN_BUILD_TRACE_MODULES=1: a Meteor build writes it at gigabytes a
|
||||
# minute - 1.1 GB in the first three minutes of a menu dev build, over
|
||||
# 12 GB in the first half hour of EVERYTHING on macOS, written twice
|
||||
# through tee - and slows the build it is tracing.
|
||||
if [ "${WEKAN_BUILD_TRACE_MODULES:-0}" = 1 ]; then
|
||||
export NODE_DEBUG="${NODE_DEBUG:+$NODE_DEBUG,}module"
|
||||
fi
|
||||
# NODE_OPTIONS parses quoted paths, including checkouts with spaces.
|
||||
export NODE_OPTIONS="${NODE_OPTIONS:-} --require=\"$(pwd)/tools/build-command-output.cjs\""
|
||||
echo "Build diagnostics: resolver command/output tracing, Node module resolution, npm verbose output, foreground install scripts, METEOR_PROFILE=$METEOR_PROFILE (milliseconds)."
|
||||
echo "Build diagnostics: resolver command/output tracing, npm verbose output, foreground install scripts, METEOR_PROFILE=$METEOR_PROFILE (milliseconds), Node module resolution $([ "${WEKAN_BUILD_TRACE_MODULES:-0}" = 1 ] && echo on || echo 'off (WEKAN_BUILD_TRACE_MODULES=1 turns it on)')."
|
||||
build_stage "1/4 Remove dependencies and build caches" rm -rf node_modules node_modules/.cache .meteor/local .build _build || return $?
|
||||
# Updating npm metadata has historically been best effort; installation is required.
|
||||
build_stage "2/4 Compile app to resolve Meteor plugin npm dependencies" meteor update --npm || echo "WARNING: Meteor plugin npm dependency compilation failed; trying dependency installation."
|
||||
@@ -645,8 +652,18 @@ function run_playwright_docker(){
|
||||
local docker_base_url="${WEKAN_BASE_URL:-http://127.0.0.1:3000}"
|
||||
local docker_mongo_url="${WEKAN_MONGO_URL:-mongodb://127.0.0.1:3001/meteor}"
|
||||
local docker_host_proxy_port=""
|
||||
local docker_network="host"
|
||||
# Docker Desktop on macOS keeps the host outside the container's loopback.
|
||||
if [ "$(uname -s)" = Darwin ]; then
|
||||
# Its host network loses connections to host.docker.internal: from a
|
||||
# container on that network about 1 connect in 75 never completed, so a
|
||||
# test page waited 60 s for a document that never came (Firefox, 2026-11-01:
|
||||
# 8 of 600 direct connects, against 0 of 1000 from the bridge network).
|
||||
# Nothing here needs the host network on macOS - WeKan and Mongo are reached
|
||||
# through host.docker.internal and SMTP through docker exec - so the
|
||||
# container uses Docker's bridge network, where the proxy below still
|
||||
# serves localhost.
|
||||
docker_network="bridge"
|
||||
# Meteor's dynamic imports use ROOT_URL (localhost in the test server).
|
||||
# Keep the browser on that same origin and bridge its loopback to the host.
|
||||
# Browsing host.docker.internal while ROOT_URL is localhost makes Firefox
|
||||
@@ -670,35 +687,53 @@ function run_playwright_docker(){
|
||||
mkdir -p "$filesroot"
|
||||
echo "Running Playwright $browser in Docker ($image)."
|
||||
echo "Expecting WeKan at $docker_base_url."
|
||||
local smtp_proxy_pid="" smtp_proxy_ready="" container_name="wekan-playwright-${browser}-$$"
|
||||
if [ "$(uname -s)" = Darwin ] && [ -n "${WEKAN_TEST_SMTP_PORT:-}" ]; then
|
||||
smtp_proxy_ready="$(mktemp "$WEKAN_DIR/.tools/tmp/smtp-proxy.XXXXXX")"
|
||||
node "$pwdir/helpers/docker-smtp-proxy.cjs" "$container_name" "$WEKAN_TEST_SMTP_PORT" "$smtp_proxy_ready" &
|
||||
smtp_proxy_pid=$!
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
[ -s "$smtp_proxy_ready" ] && break
|
||||
kill -0 "$smtp_proxy_pid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
local container_name="wekan-playwright-${browser}-$$"
|
||||
# Docker Desktop keeps the container's loopback from the host, so a server a
|
||||
# spec starts in the container cannot be reached by WeKan outside it. Each
|
||||
# port below is forwarded from the host into the container through docker
|
||||
# exec (helpers/docker-smtp-proxy.cjs, which carries any TCP): the test SMTP
|
||||
# server, and a small pool for the mock Jira/GitLab servers of the list-sync
|
||||
# specs, which take a free port from WEKAN_TEST_REVERSE_PORTS
|
||||
# (helpers/reachable-server.cjs). Elsewhere they listen on any port.
|
||||
local forward_pids="" forward_ready="" reverse_ports=""
|
||||
if [ "$(uname -s)" = Darwin ]; then
|
||||
reverse_ports="${WEKAN_TEST_REVERSE_PORTS:-39101 39102 39103 39104}"
|
||||
local fport fready fpid i
|
||||
for fport in ${WEKAN_TEST_SMTP_PORT:-} $reverse_ports; do
|
||||
fready="$(mktemp "$WEKAN_DIR/.tools/tmp/docker-port-proxy.XXXXXX")"
|
||||
node "$pwdir/helpers/docker-smtp-proxy.cjs" "$container_name" "$fport" "$fready" &
|
||||
fpid=$!
|
||||
forward_pids="$forward_pids $fpid"; forward_ready="$forward_ready $fready"
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
[ -s "$fready" ] && break
|
||||
kill -0 "$fpid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
if [ ! -s "$fready" ]; then
|
||||
kill $forward_pids 2>/dev/null; wait $forward_pids 2>/dev/null
|
||||
rm -f $forward_ready
|
||||
echo "ERROR: local test proxy for port $fport did not start."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
if [ ! -s "$smtp_proxy_ready" ]; then
|
||||
kill "$smtp_proxy_pid" 2>/dev/null; wait "$smtp_proxy_pid" 2>/dev/null
|
||||
rm -f "$smtp_proxy_ready"
|
||||
echo "ERROR: local SMTP test proxy did not start."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
# Mount the whole repo so specs that reach the repo-root node_modules
|
||||
# (e.g. @wekanteam/exceljs) and .tools resolve; run from tests/playwright.
|
||||
# The repo and the files root are mounted at their OWN host paths, so a path
|
||||
# names the same file for the specs in here and for the server outside:
|
||||
# admin-continuous-backup gives the server a key file it wrote and reads the
|
||||
# stream the server wrote, and with the repo at /repo neither side found the
|
||||
# other's file.
|
||||
# Run as the host user (--user) with a writable HOME so the container does
|
||||
# not leave root-owned files under test-results/ (which would later make
|
||||
# native Chromium/Firefox runs fail with "EACCES: permission denied, mkdir
|
||||
# .../test-results/.playwright-artifacts-N").
|
||||
docker_exec run --rm --init --ipc=host --network host \
|
||||
docker_exec run --rm --init --ipc=host --network "$docker_network" \
|
||||
--name "$container_name" \
|
||||
--label org.wekan.test-run=everything \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
-e HOME=/repo/.tools/tmp \
|
||||
-e TMPDIR=/repo/.tools/tmp \
|
||||
-e HOME="$reporoot/.tools/tmp" \
|
||||
-e TMPDIR="$reporoot/.tools/tmp" \
|
||||
-e WEKAN_BASE_URL="$docker_base_url" \
|
||||
-e WEKAN_DOCKER_HOST_PROXY_PORT="$docker_host_proxy_port" \
|
||||
-e WEKAN_MONGO_URL="$docker_mongo_url" \
|
||||
@@ -710,16 +745,17 @@ function run_playwright_docker(){
|
||||
-e WEKAN_TEST_SERIAL="${WEKAN_TEST_SERIAL:-0}" \
|
||||
-e WEKAN_TEST_SERVER_MODE="${WEKAN_TEST_SERVER_MODE:-bundle}" \
|
||||
-e WEKAN_TEST_SMTP_PORT="${WEKAN_TEST_SMTP_PORT:-}" \
|
||||
-e WEKAN_TEST_REVERSE_PORTS="$reverse_ports" \
|
||||
-e PLAYWRIGHT_HTML_OPEN=never \
|
||||
-e WEKAN_FILES_PATH=/wekan-files \
|
||||
-e WEKAN_FILES_PATH="$filesroot" \
|
||||
-e PLAYWRIGHT_JSON_OUTPUT_NAME="${PLAYWRIGHT_JSON_OUTPUT_NAME:-}" \
|
||||
-v "$filesroot":/wekan-files \
|
||||
-v "$reporoot":/repo -w /repo/tests/playwright \
|
||||
-v "$filesroot":"$filesroot" \
|
||||
-v "$reporoot":"$reporoot" -w "$pwdir" \
|
||||
"$image" \
|
||||
sh -c '
|
||||
export PATH=/repo/tests/playwright/node_modules/.bin:$PATH
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
if [ -n "$WEKAN_DOCKER_HOST_PROXY_PORT" ]; then
|
||||
node /repo/tests/playwright/helpers/docker-host-proxy.cjs &
|
||||
node "$PWD/helpers/docker-host-proxy.cjs" &
|
||||
proxy_pid=$!
|
||||
trap '\''kill "$proxy_pid" 2>/dev/null || true; wait "$proxy_pid" 2>/dev/null || true'\'' EXIT
|
||||
ready=0
|
||||
@@ -732,9 +768,9 @@ function run_playwright_docker(){
|
||||
npx playwright test --project="$0" "$@"
|
||||
' "$browser" "$@"
|
||||
local playwright_rc=$?
|
||||
if [ -n "$smtp_proxy_pid" ]; then
|
||||
kill "$smtp_proxy_pid" 2>/dev/null; wait "$smtp_proxy_pid" 2>/dev/null
|
||||
rm -f "$smtp_proxy_ready"
|
||||
if [ -n "$forward_pids" ]; then
|
||||
kill $forward_pids 2>/dev/null; wait $forward_pids 2>/dev/null
|
||||
rm -f $forward_ready
|
||||
fi
|
||||
return "$playwright_rc"
|
||||
}
|
||||
@@ -1506,7 +1542,11 @@ function run_all_tests(){
|
||||
echo "ERROR: nothing is listening on :$TEST_DB_PORT and no mongod was found (Meteor dev_bundle or PATH)."
|
||||
echo " Meteor's bundled mongod is normally at <dev_bundle>/mongodb/bin/mongod."; rm -rf "$STATDIR"; return 1
|
||||
fi
|
||||
local DBPATH="../mongodb-test-$TEST_DB_PORT"
|
||||
# In .tools/, where everything this script writes lives (CLAUDE.md;
|
||||
# tests/toolsDirOnly.test.cjs). It was in the checkout's PARENT directory,
|
||||
# which a sandboxed editor cannot write - the test database never
|
||||
# started there.
|
||||
local DBPATH="$WEKAN_DIR/.tools/mongodb-test-$TEST_DB_PORT"
|
||||
mkdir -p "$DBPATH"
|
||||
echo "==> Starting MongoDB (Meteor's mongod) on :$TEST_DB_PORT, dbpath $DBPATH."
|
||||
{ echo "===== mongod :$TEST_DB_PORT - started $(date '+%Y-%m-%d %H:%M:%S %Z') ====="; exec "$MONGOD_BIN" --port "$TEST_DB_PORT" --dbpath "$DBPATH" --bind_ip 127.0.0.1 --nounixsocket; } > "$RUN_LOGDIR/wekan-test-mongod.log" 2>&1 &
|
||||
@@ -1534,7 +1574,7 @@ function run_all_tests(){
|
||||
export WEKAN_FILES_PATH_HOST="$WRITABLE_ABS/files"
|
||||
# ...and the same path for a LOCAL playwright run, not only the Docker one.
|
||||
# Only WEKAN_FILES_PATH_HOST was exported, which is read when the browsers run
|
||||
# in a container (it is mounted there as /wekan-files). A local run fell back
|
||||
# in a container (it is mounted there at the same path). A local run fell back
|
||||
# to the spec's own default of .build/bundle/files, so a spec that writes a
|
||||
# stored attachment wrote it OUTSIDE the root this server reads - and then
|
||||
# tested what WeKan does with a file it does not own.
|
||||
@@ -2393,8 +2433,8 @@ function git_fix_changelog_links(){
|
||||
local script="$WEKAN_DIR/releases/fix-changelog-hashes.sh"
|
||||
[ -f "$script" ] || return 1
|
||||
bash "$script" || return 1
|
||||
if [ -n "$(git status --porcelain -- CHANGELOG.md 2>/dev/null)" ]; then
|
||||
git add CHANGELOG.md
|
||||
if [ -n "$(git status --porcelain -- CHANGELOG-big.md CHANGELOG.md 2>/dev/null)" ]; then
|
||||
git add CHANGELOG-big.md CHANGELOG.md
|
||||
git commit -q -m "CHANGELOG: repoint commit links after history moved.
|
||||
|
||||
A rebase rewrote the commits these entries link to, so the links named commits
|
||||
@@ -2605,7 +2645,7 @@ choose() {
|
||||
# snap/bundle, not on a maintainer's machine) and the superseded old-*.sh and
|
||||
# translations/fill_translations.py.
|
||||
# Release notes summarize translations as affected languages; other changelog
|
||||
# details stay in CHANGELOG.md. Notes include only In short, Security,
|
||||
# details stay in CHANGELOG-big.md. Notes include only In short, Security,
|
||||
# Translations language list, thanks and the changelog link. Binary provenance remains
|
||||
# a build artifact and must not be appended by any release entry point.
|
||||
RELEASE_SCRIPTS=( "Release|Release All: audit, commit, push and build with Actions|releases/release-all.sh|||"
|
||||
|
||||
@@ -111,6 +111,28 @@ Meteor.startup(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// LOGOUT_IDLE_MINUTES: report the user's activity while signed in, if the
|
||||
// limit is set (models/lib/idleLogout.js).
|
||||
const { createIdleActivityReporter } = require('/client/lib/idleActivity');
|
||||
const idleReporter = createIdleActivityReporter({
|
||||
target: document,
|
||||
touch: () => Meteor.call('touchActivity', () => {}),
|
||||
});
|
||||
let idleCheckedFor = null;
|
||||
Tracker.autorun(() => {
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
idleCheckedFor = null;
|
||||
idleReporter.stop();
|
||||
return;
|
||||
}
|
||||
if (idleCheckedFor === userId) return;
|
||||
idleCheckedFor = userId;
|
||||
Meteor.call('getIdleLogout', (error, result) => {
|
||||
if (!error && result && result.enabled && Meteor.userId() === userId) idleReporter.start();
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize mobile mode on startup for iOS devices
|
||||
// This ensures mobile mode is applied correctly on page load
|
||||
Tracker.afterFlush(() => {
|
||||
|
||||
@@ -366,43 +366,43 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* The title is two things at once, split like a minicard's: the LEADING half
|
||||
opens the editor, the trailing half drags the window. With drag handles OFF
|
||||
there is no handle, so the title bar is what the window is moved by - and a
|
||||
heading that is entirely an edit target leaves nothing to take hold of.
|
||||
/* #6839: the whole title edits - every word of it and the space beside a
|
||||
short last line, which is what anybody clicks to change the title. The window
|
||||
is moved from the empty space ABOVE the title, in the row of the collapse
|
||||
caret and the menu (cardDetails.js), or by the drag handle when drag handles
|
||||
are on. It was split like a minicard's - the trailing half dragged - and a
|
||||
click on the right of the title did nothing anybody expected.
|
||||
|
||||
An overlay rather than a split of the text, because the text is a `.viewer`
|
||||
and cutting it in two would cut the sentence in two. `inset-inline-start`, so
|
||||
the edit half is the left in English and the right in Arabic from one rule.
|
||||
The drag handler steps aside for this zone (cardDetails.js). */
|
||||
An overlay rather than a link around the text, because the text is a
|
||||
`.viewer` with its own links. */
|
||||
.card-details .card-details-header .card-details-title .card-details-title-edit-zone {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
/* Half of the heading - and the heading is stretched to the row below, so
|
||||
this is half of the BAR however long the title happens to be. */
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
cursor: text;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* The heading fills the row rather than shrinking to its text, so "half the
|
||||
title" and "half the bar" are the same half however long the title is. A
|
||||
heading only as wide as its words leaves the empty space beside it belonging
|
||||
to nothing - and that space is where anybody would grab to move the window.
|
||||
It stays inline-block so the complete-toggle beside it keeps its place; the
|
||||
buttons floating at the trailing edge take their room out of this width. */
|
||||
/* The heading fills the row rather than shrinking to its text, so the edit
|
||||
zone covers the space beside a short title too. It stays inline-block so
|
||||
the complete-toggle beside it keeps its place. */
|
||||
.card-details .card-details-header .card-details-title {
|
||||
min-width: 60%;
|
||||
}
|
||||
|
||||
/* With drag handles ON the handle is the only drag source, so the whole title
|
||||
is the edit target - the same rule the minicard follows, from the same
|
||||
question: `card-details-with-handle` is set by the template from
|
||||
isTouchScreenOrShowDesktopDragHandles, which is Utils.showDragHandles(). */
|
||||
.card-details.card-details-with-handle .card-details-header .card-details-title .card-details-title-edit-zone {
|
||||
width: 100%;
|
||||
/* The empty part of the header row is what moves the window when there is no
|
||||
drag handle; with one, the handle alone does. */
|
||||
.card-details:not(.card-details-with-handle) .card-details-header {
|
||||
cursor: move;
|
||||
}
|
||||
.card-details .card-details-header .card-details-title,
|
||||
.card-details .card-details-header a {
|
||||
cursor: auto;
|
||||
}
|
||||
.card-details .card-details-header a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-details .card-details-header .linked-card-location {
|
||||
font-style: italic;
|
||||
|
||||
@@ -49,10 +49,9 @@ template(name="cardDetails")
|
||||
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
//- The handle appears only when drag handles are ON, like the
|
||||
minicard's. With them off the window is dragged by its own title bar
|
||||
- the trailing half of the title - so a handle would be a second way
|
||||
to do what the bar already does; with them on it is the ONLY way, and
|
||||
the title is all edit target.
|
||||
minicard's. With them off the window is dragged by the empty space
|
||||
of its header row, so a handle would be a second way to do what the
|
||||
bar already does; with them on it is the ONLY way.
|
||||
client/components/cards/minicard.jade
|
||||
if canModifyCard
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
@@ -80,12 +79,12 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsDueComplete
|
||||
a.card-details-complete-toggle.js-toggle-due-complete(title="{{#if getDueComplete}}{{_ 'card-mark-incomplete'}}{{else}}{{_ 'card-mark-complete'}}{{/if}}")
|
||||
.materialCheckBox(class="{{#if getDueComplete}}is-checked{{/if}}")
|
||||
//- The title is BOTH: the leading half opens the editor and the
|
||||
trailing half drags the window, exactly as a minicard's title splits
|
||||
between editing and dragging the card. `js-card-title-drag-handle`
|
||||
stays on the whole heading - the drag handler is what steps aside for
|
||||
the edit zone - so a reader who may not write can still drag the
|
||||
window by any part of it, which is what that class was for.
|
||||
//- #6839: the whole title opens the editor. The window is moved from
|
||||
the empty space of the header row above it, or by the drag handle
|
||||
when drag handles are on. `js-card-title-drag-handle` stays on the
|
||||
whole header - the drag handler is what steps aside for the edit zone
|
||||
- so a reader who may not write, who has no edit zone, can still drag
|
||||
the window by the title.
|
||||
if showLabelsAboveTitleOnCard
|
||||
.card-details-labels-above-title
|
||||
+minicardLabels(show=labelsShownAboveTitle labels=labels hideText=hiddenCardLabelText)
|
||||
|
||||
@@ -701,9 +701,12 @@ Template.cardDetails.onDestroyed(function () {
|
||||
|
||||
// #6686: see the comment in onCreated above. Only close a popup that is
|
||||
// still showing THIS card's data at the base of its stack - a popup opened
|
||||
// for an unrelated card/context must not be touched.
|
||||
// for an unrelated card/context must not be touched. #6840: and only when
|
||||
// this card is no longer the open one - closed, or another card opened. A
|
||||
// card whose details are merely rendered again is still open, and its popup
|
||||
// (the Due date one `d` just opened) must not flash shut.
|
||||
const openedCardId = this.openedCardId;
|
||||
if (openedCardId && Popup.isOpen()) {
|
||||
if (openedCardId && Popup.isOpen() && Utils.getCurrentCardId() !== openedCardId) {
|
||||
const baseStackEntry = Popup._stack?.[0];
|
||||
if (baseStackEntry?.dataContext?._id === openedCardId) {
|
||||
Popup.close();
|
||||
@@ -1287,17 +1290,18 @@ Template.cardDetails.events({
|
||||
// selects text instead of moving the card window.
|
||||
return;
|
||||
}
|
||||
// ...and don't drag from the half that EDITS. The title is split like a
|
||||
// minicard's: the leading half opens the editor, the trailing half is what
|
||||
// you take hold of. With drag handles ON the zone covers the whole title
|
||||
// (cardDetails.css), so the window is dragged by its handle alone - which is
|
||||
// what turning handles on means.
|
||||
// #6839: ...and never from the title, which edits - all of it
|
||||
// (cardDetails.css). The window is moved from the empty space of the
|
||||
// header row above it, beside the collapse caret and the menu.
|
||||
if ($(event.target).closest('.card-details-title-edit-zone').length > 0) {
|
||||
return;
|
||||
}
|
||||
// The drag handle has its own handler; without this both would run and the
|
||||
// window would be moved twice by one press.
|
||||
if ($(event.target).closest('.js-card-drag-handle').length > 0) {
|
||||
// window would be moved twice by one press. And when there IS a handle -
|
||||
// drag handles on - it is the only thing that moves the window, which is
|
||||
// what turning handles on means.
|
||||
if ($(event.target).closest('.js-card-drag-handle').length > 0
|
||||
|| $(event.target).closest('.card-details').find('.js-card-drag-handle').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -374,7 +374,12 @@
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
margin-inline-end: 84px;
|
||||
/* #6834: room for the drag handle beside it (20px, 1px from the edge), and
|
||||
no more. It reserved 84px - a third of a 250px minicard - for nothing:
|
||||
the avatars float AFTER this row and `clear: both` starts it below the
|
||||
menu, so a due date "1405-08-10 12:00 (1 day left)" was broken over three
|
||||
lines beside an empty strip. */
|
||||
margin-inline-end: 24px;
|
||||
clear: both;
|
||||
}
|
||||
.minicard .date {
|
||||
|
||||
@@ -28,4 +28,4 @@ template(name="cardWhiteboard")
|
||||
svg.card-whiteboard-surface.js-whiteboard-surface(viewBox="0 0 {{width}} {{height}}" preserveAspectRatio="xMidYMid meet" class="{{#if canDraw}}can-draw{{/if}}")
|
||||
rect(width=width height=height fill="#ffffff")
|
||||
each strokes
|
||||
path(d=pathD stroke=color stroke-width=width fill="none" stroke-linecap="round" stroke-linejoin="round")
|
||||
path(d=pathD stroke=color stroke-width=strokeWidth fill="none" stroke-linecap="round" stroke-linejoin="round")
|
||||
@@ -116,7 +116,11 @@ Template.cardWhiteboard.helpers({
|
||||
const current = tpl.current.get();
|
||||
return [...saved, ...pending, ...(current ? [current] : [])]
|
||||
.filter(s => s && Array.isArray(s.points) && s.points.length)
|
||||
.map(s => ({ color: s.color, width: s.width, pathD: pathD(s.points) }));
|
||||
// strokeWidth, not width: inside `each strokes` Spacebars looks a name up
|
||||
// among this template's helpers before the stroke's own fields, and the
|
||||
// width helper is the drawing's (800) - every line was drawn 800 wide and
|
||||
// covered the whiteboard.
|
||||
.map(s => ({ color: s.color, strokeWidth: s.width, pathD: pathD(s.points) }));
|
||||
},
|
||||
saving() { return Template.instance().saving.get(); },
|
||||
message() { return Template.instance().message.get(); },
|
||||
|
||||
@@ -102,6 +102,11 @@
|
||||
padding-inline: 4px;
|
||||
}
|
||||
|
||||
/* #6838: a word, not an arrow - it may be wider than the arrows beside it. */
|
||||
.selected-calendar-heading .js-calendar-today {
|
||||
white-space: nowrap;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
.selected-calendar-heading .selected-calendar-date {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
@@ -22,6 +22,9 @@ template(name="calendarDateInput")
|
||||
| {{monthLabel}}
|
||||
if inline
|
||||
span.selected-calendar-date {{headingDateText}}
|
||||
//- #6838: back to today's month from wherever the arrows went. It only
|
||||
//- shows the month; the date stays as it is until a day is clicked.
|
||||
button.primary.js-calendar-today(type="button" aria-label="{{_ 'today'}}") {{_ 'today'}}
|
||||
button.primary.js-calendar-next(type="button" aria-label="{{_ 'next'}} {{_ 'month'}}") ›
|
||||
button.primary.js-calendar-year-next(type="button" aria-label="{{_ 'next'}} {{_ 'predicate-year'}}") »
|
||||
table
|
||||
@@ -34,5 +37,5 @@ template(name="calendarDateInput")
|
||||
tr
|
||||
each days
|
||||
td(class="{{#if this.today}}is-today-cell{{/if}}")
|
||||
if this
|
||||
unless this.empty
|
||||
button.primary.js-calendar-day(type="button" class="{{#if today}}is-today{{/if}}" data-date=iso title=title aria-label=title aria-pressed=selected aria-current=ariaCurrent tabindex=tabIndex) {{label}}
|
||||
@@ -86,9 +86,17 @@ Template.calendarDateInput.helpers({
|
||||
// the browser's local day, the same local calendar day formatDate() gives
|
||||
// every other cell, so it is the cell the user means by "today".
|
||||
const todayIso = formatDate(new Date());
|
||||
// #6838: every row and every cell - an empty one too - has its own _id.
|
||||
// Blaze matches list items by _id; without one, going from one month to
|
||||
// another reused cells by position and the empty cells, all alike, were
|
||||
// moved around: October came back as "29, empty, 30, empty", without the
|
||||
// 31st. The month is part of each id, so a new month is new cells.
|
||||
const month = days.find(Boolean);
|
||||
const monthId = month ? formatDate(month) : 'empty';
|
||||
const rows = [];
|
||||
for (let index = 0; index < days.length; index += 7) {
|
||||
rows.push({ days: days.slice(index, index + 7).map(date => date && ({
|
||||
rows.push({ _id: `${monthId}-week-${index / 7}`, days: days.slice(index, index + 7).map((date, cell) => (date ? {
|
||||
_id: formatDate(date),
|
||||
iso: formatDate(date),
|
||||
today: formatDate(date) === todayIso,
|
||||
// null omits the attribute: only today's button carries aria-current.
|
||||
@@ -97,7 +105,7 @@ Template.calendarDateInput.helpers({
|
||||
title: formatDateForDisplay(date, false),
|
||||
selected: tpl.selectedDate.get() && formatDate(tpl.selectedDate.get()) === formatDate(date),
|
||||
tabIndex: focusDate && formatDate(focusDate) === formatDate(date) ? 0 : -1,
|
||||
})) });
|
||||
} : { _id: `${monthId}-empty-${index + cell}`, empty: true })) });
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
@@ -121,6 +129,11 @@ Template.calendarDateInput.events({
|
||||
event.preventDefault();
|
||||
tpl.shownDate.set(calendarMonthRange(tpl.shownDate.get(), selectedSystem().intl).end);
|
||||
},
|
||||
'click .js-calendar-today'(event, tpl) {
|
||||
event.preventDefault();
|
||||
tpl.shownDate.set(new Date());
|
||||
Tracker.afterFlush(() => tpl.find('.js-calendar-day.is-today')?.focus());
|
||||
},
|
||||
'click .js-calendar-year-previous'(event, tpl) { event.preventDefault(); tpl.shownDate.set(shiftCalendarYear(tpl.shownDate.get(), -1, selectedSystem().intl)); },
|
||||
'click .js-calendar-year-next'(event, tpl) { event.preventDefault(); tpl.shownDate.set(shiftCalendarYear(tpl.shownDate.get(), 1, selectedSystem().intl)); },
|
||||
'click .js-calendar-day'(event, tpl) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Markdown } from 'meteor/wekan-markdown';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { memberMatchesTerm } from '/models/lib/memberAutocomplete';
|
||||
import { internalCardPath } from '/models/lib/internalCardLink';
|
||||
import { parseAllowedUrlSchemes, hrefScheme, BUILT_IN } from '/models/lib/urlSchemeAllowlist';
|
||||
import { parseAllowedUrlSchemes, hrefScheme, BUILT_IN, NEVER_LINKED } from '/models/lib/urlSchemeAllowlist';
|
||||
import autosize from 'autosize';
|
||||
var converter = require('@wekanteam/html-to-markdown');
|
||||
|
||||
@@ -348,6 +348,10 @@ Template.viewer.events({
|
||||
const scheme = hrefScheme(href);
|
||||
if (cardPath) {
|
||||
FlowRouter.go(cardPath);
|
||||
} else if (NEVER_LINKED.has(scheme)) {
|
||||
// The sanitizer never lets one of these be a link; should one reach
|
||||
// the page some other way, a click still does not launch its
|
||||
// handler (ms-msdt:, search-ms:, telnet: ...).
|
||||
} else if (scheme && !BUILT_IN.includes(scheme)) {
|
||||
// #3218: an allowed custom scheme (thunderlink:, onenote:, ...) is
|
||||
// handed to the application registered for it by following the
|
||||
|
||||
@@ -7,7 +7,7 @@ template(name="historyRecoveryNotice")
|
||||
.history-recovery-notice.js-history-recovery-notice(role="status" aria-live="polite")
|
||||
p {{_ pending.messageKey}}
|
||||
p.quiet {{_ 'history-request-hint'}}
|
||||
button.primary.js-history-retry(type="button" disabled=busy) {{_ 'history-request-retry'}}
|
||||
button.primary.js-history-retry(type="button" disabled=retryDisabled) {{_ 'history-request-retry'}}
|
||||
button.js-history-forget(type="button" disabled=busy) {{_ 'history-request-forget'}}
|
||||
if checkpoint
|
||||
.history-recovery-notice.history-checkpoint-notice.js-history-checkpoint-notice(role="status" aria-live="polite" class=checkpointPlacement)
|
||||
|
||||
@@ -52,6 +52,13 @@ Template.historyRecoveryNotice.helpers({
|
||||
busy() {
|
||||
return Template.instance().busy.get();
|
||||
},
|
||||
// undoRedoLast does nothing for someone who may not write on the board, and
|
||||
// right after a reload that is everyone until the board and its members
|
||||
// arrive: the notice shows first, and a Retry clicked then was dropped
|
||||
// without a word. It is enabled once the retry can act.
|
||||
retryDisabled() {
|
||||
return Template.instance().busy.get() || !Utils.canModifyBoard();
|
||||
},
|
||||
});
|
||||
|
||||
async function resolve(tpl, action) {
|
||||
|
||||
@@ -55,7 +55,7 @@ template(name="userFormsLayout")
|
||||
+loader
|
||||
else
|
||||
// ARIA live region for error messages
|
||||
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f; margin-bottom: 1em;")
|
||||
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f; margin-bottom: 1em;")= loginErrorText
|
||||
// Second sign-in step for accounts-2fa (issue #3058). Hidden unless a
|
||||
// password login comes back with the "no-2fa-code" / "invalid-2fa-code"
|
||||
// error - see decideTwoFactorLoginStep() in
|
||||
|
||||
@@ -303,6 +303,7 @@ Template.userFormsLayout.onDestroyed(function () {
|
||||
});
|
||||
|
||||
Template.userFormsLayout.helpers({
|
||||
loginErrorText() { return loginErrorText.get(); },
|
||||
passkeysSupported() { return require('/client/lib/passkeys').passkeysSupported(); },
|
||||
passkeyMessage() { return Template.instance().passkeyMessage.get(); },
|
||||
windowsSsoOffered() { return Meteor.settings?.public?.windowsSsoEnabled === true; },
|
||||
@@ -557,20 +558,23 @@ Tracker.autorun(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// The message in #login-error-message, rendered by the template as text. Not
|
||||
// written into the element: it is inside `if isLoading`, and when that flips
|
||||
// Blaze draws a new, empty element - a SAML popup error shown just before
|
||||
// then was gone (saml-popup-error.e2e.js, Firefox). Text, never HTML.
|
||||
const loginErrorText = new ReactiveVar('');
|
||||
function showLoginError(err) {
|
||||
const errorDiv = document.getElementById('login-error-message');
|
||||
if (!errorDiv) return;
|
||||
if (!err) {
|
||||
errorDiv.textContent = '';
|
||||
loginErrorText.set('');
|
||||
return;
|
||||
}
|
||||
const reason = err.reason || err.message || '';
|
||||
// #2460: an account its member locked to SQRL says so in their language.
|
||||
const translated = ['oauth-account-conflict', 'sqrl-only', 'sqrl-hardlock']
|
||||
.find(key => err.error === key || reason === key);
|
||||
errorDiv.textContent = translated
|
||||
loginErrorText.set(translated
|
||||
? TAPi18n.__(translated)
|
||||
: reason || String(err.error || err);
|
||||
: reason || String(err.error || err));
|
||||
}
|
||||
|
||||
Template.defaultLayout.events({
|
||||
|
||||
@@ -1446,9 +1446,18 @@ body.mobile-mode .pop-over .content-wrapper {
|
||||
cursor: move;
|
||||
color: #888;
|
||||
}
|
||||
.pop-over .header:has(.popup-drag-handle) {
|
||||
/* #6839: the title bar moves every popup while drag handles are off; with
|
||||
them on, the handle does, and the bar is an ordinary title. */
|
||||
.pop-over .header {
|
||||
cursor: move;
|
||||
}
|
||||
.pop-over .header:has(.popup-drag-handle) {
|
||||
cursor: auto;
|
||||
}
|
||||
.pop-over .header a,
|
||||
.pop-over .header button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.pop-over .header .popup-maximize-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -29,8 +29,11 @@ function resizeDatePopup(element, width, height) {
|
||||
Object.entries(styles).forEach(([key, value]) => element.style.setProperty(key, value, 'important'));
|
||||
}
|
||||
|
||||
// Popups dragged by their title bar (or the drag handle beside it), as the
|
||||
// opened card is. A date popup is movable too, found by its .edit-date.
|
||||
// #6839: EVERY popup is moved like the opened card: by its title bar when drag
|
||||
// handles are off, by the drag handle before its title when they are on - the
|
||||
// handle alone, then, as the card's. These are the popups that may also GROW
|
||||
// past the window while open (Drawing replacing a menu), so they are kept
|
||||
// inside it as they grow; so is any popup the user has dragged.
|
||||
const MOVABLE_POPUPS = new Set(['boardViewSettingsPopup', 'cardDrawingPopup', 'boardCardSettingsPopup']);
|
||||
// Popups with the opened card's maximize / minimize button.
|
||||
const MAXIMIZABLE_POPUPS = new Set(['cardDrawingPopup', 'boardCardSettingsPopup']);
|
||||
@@ -66,7 +69,7 @@ Popup.template.onCreated(function () {
|
||||
|
||||
Popup.template.helpers({
|
||||
popupMovable() {
|
||||
return MOVABLE_POPUPS.has(this.popupName);
|
||||
return true;
|
||||
},
|
||||
popupMaximizable() {
|
||||
return MAXIMIZABLE_POPUPS.has(this.popupName);
|
||||
@@ -87,21 +90,43 @@ Popup.template.events({
|
||||
'pointerdown .header'(evt, tpl) {
|
||||
const element = evt.currentTarget.closest('.pop-over');
|
||||
// A maximized popup has no place to be dragged to.
|
||||
// With drag handles on, the handle is the only thing that moves it.
|
||||
const handle = element.querySelector('.js-popup-drag-handle');
|
||||
if (evt.button !== 0 || element.classList.contains('pop-over--maximized') ||
|
||||
(!element.querySelector('.edit-date') && !MOVABLE_POPUPS.has(element.dataset.popup)) ||
|
||||
(handle && !evt.target.closest('.js-popup-drag-handle')) ||
|
||||
evt.target.closest('a, button, input, select')) return;
|
||||
evt.preventDefault(); evt.stopPropagation();
|
||||
const bounds = element.getBoundingClientRect();
|
||||
// Pin it where it is before it moves. A popup is placed by its margins, by
|
||||
// inset-inline-end (the member menu, at the right edge) or by left/top, so
|
||||
// `left: bounds.left` alone put some of them elsewhere - one jumped across
|
||||
// the window as the drag began. Set it, see where it went, and correct.
|
||||
const place = (left, top) => {
|
||||
element.style.setProperty('inset-inline-start', 'auto', 'important');
|
||||
element.style.setProperty('inset-inline-end', 'auto', 'important');
|
||||
element.style.setProperty('left', `${left}px`, 'important');
|
||||
element.style.setProperty('top', `${top}px`, 'important');
|
||||
};
|
||||
place(bounds.left, bounds.top);
|
||||
const placed = element.getBoundingClientRect();
|
||||
const left = bounds.left + (bounds.left - placed.left);
|
||||
const top = bounds.top + (bounds.top - placed.top);
|
||||
place(left, top);
|
||||
element.dataset.userMoved = 'true';
|
||||
tpl._dateMove = { element, pointerId: evt.pointerId, x: evt.clientX, y: evt.clientY,
|
||||
left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height };
|
||||
left, top, width: bounds.width, height: bounds.height,
|
||||
// The clamp keeps the popup's VISIBLE box 12px inside the window.
|
||||
dx: bounds.left - left, dy: bounds.top - top };
|
||||
evt.currentTarget.setPointerCapture(evt.pointerId);
|
||||
},
|
||||
'pointermove .header'(evt, tpl) {
|
||||
const drag = tpl._dateMove;
|
||||
if (!drag || drag.pointerId !== evt.pointerId) return;
|
||||
evt.preventDefault(); evt.stopPropagation();
|
||||
const left = Math.max(12, Math.min(window.innerWidth - drag.width - 12, drag.left + evt.clientX - drag.x));
|
||||
const top = Math.max(12, Math.min(window.innerHeight - drag.height - 12, drag.top + evt.clientY - drag.y));
|
||||
const boxLeft = Math.max(12, Math.min(window.innerWidth - drag.width - 12, drag.left + drag.dx + evt.clientX - drag.x));
|
||||
const boxTop = Math.max(12, Math.min(window.innerHeight - drag.height - 12, drag.top + drag.dy + evt.clientY - drag.y));
|
||||
const left = boxLeft - drag.dx;
|
||||
const top = boxTop - drag.dy;
|
||||
// Dragging uses physical pointer coordinates in either text direction.
|
||||
drag.element.style.setProperty('inset-inline-start', 'auto', 'important');
|
||||
drag.element.style.setProperty('inset-inline-end', 'auto', 'important');
|
||||
@@ -221,7 +246,8 @@ Popup.template.onRendered(function () {
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const element = this._popupElement;
|
||||
this._boundsObserver = new ResizeObserver(() => {
|
||||
if (!MOVABLE_POPUPS.has(element.dataset.popup) || element.classList.contains('pop-over--maximized')) return;
|
||||
if (!(MOVABLE_POPUPS.has(element.dataset.popup) || element.dataset.userMoved)
|
||||
|| element.classList.contains('pop-over--maximized')) return;
|
||||
const box = element.getBoundingClientRect();
|
||||
const moved = keptInside(box, window.innerWidth, window.innerHeight);
|
||||
if (!moved) return;
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
.header
|
||||
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}" aria-label="{{_ 'go-back'}}")
|
||||
i.fa.fa-caret-left(aria-hidden="true")
|
||||
//- A movable popup (popup.js MOVABLE_POPUPS) is dragged by its title bar
|
||||
or this handle, like the opened card - and, like the card's, the handle
|
||||
is there only while drag handles are on; a maximizable one has the
|
||||
card's maximize / minimize button beside its close button.
|
||||
//- #6839: every popup is moved like the opened card - by its title bar
|
||||
while drag handles are off, by this handle alone while they are on
|
||||
(popup.js); a maximizable one has the card's maximize / minimize button
|
||||
beside its close button.
|
||||
if popupMovable
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.popup-drag-handle.js-popup-drag-handle(title="{{_ 'drag-to-move'}}")
|
||||
|
||||
@@ -247,6 +247,33 @@
|
||||
.rules-back .js-goback .fa-arrow-left {
|
||||
color: #fff !important;
|
||||
}
|
||||
/* #6852: Back to the board, at the top of the Rules page. */
|
||||
.rules-back-to-board {
|
||||
margin-block: 8px;
|
||||
margin-inline: 14px;
|
||||
}
|
||||
/* A link, so it opens the board as a link does (also in a new tab), drawn as
|
||||
the primary buttons around it: forms.css styles only button.primary. */
|
||||
.rules-back-to-board .js-rules-back-to-board {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 20px;
|
||||
border-radius: 3px;
|
||||
background: var(--theme-accent, #005377);
|
||||
box-shadow: 0 1px 0 #4d4d4d;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
.rules-back-to-board .js-rules-back-to-board:hover,
|
||||
.rules-back-to-board .js-rules-back-to-board:focus {
|
||||
background: var(--theme-accent, #004766);
|
||||
color: #fff;
|
||||
}
|
||||
.rules-back-to-board .js-rules-back-to-board .fa-arrow-left {
|
||||
color: #fff !important;
|
||||
}
|
||||
.flex,
|
||||
.triggers-content .triggers-body .triggers-side-menu ul li a {
|
||||
display: -webkit-box;
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
template(name="rulesMain")
|
||||
.wrapper
|
||||
.rules-page(class=currentBoard.colorClass)
|
||||
//- #6852: the way back to the board. The trigger, action and rule
|
||||
details pages have their own Back, to the rules list, so this one is
|
||||
shown only where there is none - two Backs going to two places would
|
||||
be one too many.
|
||||
if showBackToBoard
|
||||
.rules-back-to-board
|
||||
a.js-rules-back-to-board(href="{{pathFor 'board' id=currentBoard._id slug=currentBoard.slug}}" title="{{currentBoard.title}}")
|
||||
i.fa.fa-arrow-left
|
||||
| {{_ 'back'}}
|
||||
//- #2217: who may edit sees every view; a member a board shows its Rules
|
||||
to reads the rules list and rule details, with no control that
|
||||
changes a rule; anybody else sees nothing (client/lib/rulesAccess.js).
|
||||
|
||||
@@ -44,6 +44,10 @@ Template.rulesMain.helpers({
|
||||
rulesCurrentTab() {
|
||||
return Template.instance().rulesCurrentTab;
|
||||
},
|
||||
// #6852: every Rules page but the three that already go Back to the list.
|
||||
showBackToBoard() {
|
||||
return !['trigger', 'action', 'ruleDetails'].includes(Template.instance().rulesCurrentTab.get());
|
||||
},
|
||||
ruleName() {
|
||||
return Template.instance().ruleName;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.admin-audit-log-search { margin: 0.5rem 0; max-width: 24rem; }
|
||||
.admin-audit-log-table { width: 100%; border-collapse: collapse; }
|
||||
.admin-audit-log-table th, .admin-audit-log-table td {
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.3);
|
||||
text-align: start;
|
||||
vertical-align: top;
|
||||
}
|
||||
.admin-audit-args { white-space: pre-wrap; word-break: break-all; font-size: 0.85em; }
|
||||
.admin-audit-failed .admin-audit-error { color: #c0392b; }
|
||||
.admin-audit-log-pager { margin-top: 0.5rem; display: flex; gap: 0.5rem; align-items: center; }
|
||||
@@ -0,0 +1,39 @@
|
||||
template(name="adminAuditLog")
|
||||
//- Admin Panel / People / Audit log (models/lib/adminAudit.js): every
|
||||
//- administrative action of a site administrator, newest first. The
|
||||
//- arguments are shown as stored - passwords and secrets never are.
|
||||
.admin-audit-log
|
||||
p {{_ 'admin-audit-log-info'}}
|
||||
.admin-audit-log-search
|
||||
input.wekan-form-control.js-admin-audit-search(type="search" placeholder="{{_ 'search'}}" value=search)
|
||||
if loading
|
||||
+spinner
|
||||
else
|
||||
table.admin-audit-log-table
|
||||
thead
|
||||
tr
|
||||
th {{_ 'admin-audit-log-when'}}
|
||||
th {{_ 'admin-audit-log-who'}}
|
||||
th {{_ 'admin-audit-log-action'}}
|
||||
th {{_ 'admin-audit-log-details'}}
|
||||
th IP
|
||||
tbody
|
||||
each rows
|
||||
tr(class="{{#unless ok}}admin-audit-failed{{/unless}}")
|
||||
td {{when at}}
|
||||
td {{actorUsername}}
|
||||
td
|
||||
code {{method}}
|
||||
unless ok
|
||||
|
|
||||
span.admin-audit-error {{error}}
|
||||
td
|
||||
code.admin-audit-args {{args}}
|
||||
td {{ip}}
|
||||
else
|
||||
tr
|
||||
td(colspan="5") {{_ 'admin-audit-log-empty'}}
|
||||
.admin-audit-log-pager
|
||||
button.js-admin-audit-prev(type="button" disabled=noPrev) ‹
|
||||
span {{page}} / {{pages}} ({{total}})
|
||||
button.js-admin-audit-next(type="button" disabled=noNext) ›
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
// Admin Panel / People / Audit log: pages of server/adminAudit.js's trail.
|
||||
Template.adminAuditLog.onCreated(function () {
|
||||
this.page = new ReactiveVar(1);
|
||||
this.search = new ReactiveVar('');
|
||||
this.result = new ReactiveVar({ rows: [], total: 0, page: 1, perPage: 50 });
|
||||
this.loading = new ReactiveVar(true);
|
||||
this.autorun(() => {
|
||||
const options = { page: this.page.get(), search: this.search.get() };
|
||||
this.loading.set(true);
|
||||
Meteor.call('adminAuditLogPage', options, (error, result) => {
|
||||
this.loading.set(false);
|
||||
if (!error && result) this.result.set(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.adminAuditLog.helpers({
|
||||
loading() { return Template.instance().loading.get(); },
|
||||
rows() { return Template.instance().result.get().rows; },
|
||||
total() { return Template.instance().result.get().total; },
|
||||
page() { return Template.instance().page.get(); },
|
||||
search() { return Template.instance().search.get(); },
|
||||
pages() {
|
||||
const { total, perPage } = Template.instance().result.get();
|
||||
return Math.max(1, Math.ceil(total / perPage));
|
||||
},
|
||||
noPrev() { return Template.instance().page.get() <= 1; },
|
||||
noNext() {
|
||||
const { total, perPage } = Template.instance().result.get();
|
||||
return Template.instance().page.get() >= Math.ceil(total / perPage);
|
||||
},
|
||||
when(at) { return at ? new Date(at).toLocaleString() : ''; },
|
||||
});
|
||||
|
||||
let searchTimer = null;
|
||||
Template.adminAuditLog.events({
|
||||
'input .js-admin-audit-search'(event, tpl) {
|
||||
const value = event.currentTarget.value;
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => { tpl.page.set(1); tpl.search.set(value); }, 300);
|
||||
},
|
||||
'click .js-admin-audit-prev'(event, tpl) { tpl.page.set(Math.max(1, tpl.page.get() - 1)); },
|
||||
'click .js-admin-audit-next'(event, tpl) { tpl.page.set(tpl.page.get() + 1); },
|
||||
});
|
||||
@@ -58,6 +58,8 @@ template(name="adminProblems")
|
||||
+eventStreamReport(stream="api")
|
||||
else if isPane 'report-office'
|
||||
+officeReport
|
||||
else if isPane 'report-password-length'
|
||||
+passwordLengthReport
|
||||
else if tablePageData
|
||||
+tablePage(tablePageData)
|
||||
|
||||
|
||||
@@ -321,6 +321,9 @@ const PROBLEMS_MENU = [
|
||||
// Security, and the two sit in the same menu.
|
||||
{ id: 'report-security', icon: 'fa-shield', labelKey: 'securityReportTitle' },
|
||||
{ id: 'report-impersonation', icon: 'fa-user-secret', labelKey: 'impersonationReportTitle' },
|
||||
// Who signs in with a password shorter than PASSWORD_MIN_LENGTH, and the two
|
||||
// settings that decide it (passwordLengthReport.js).
|
||||
{ id: 'report-password-length', icon: 'fa-key', labelKey: 'password-length-report-title' },
|
||||
{ id: 'features-performance', icon: 'fa-tachometer', labelKey: 'features-performance', emoji: true },
|
||||
{ id: 'report-speed', icon: 'fa-tachometer', labelKey: 'speedReportTitle' },
|
||||
{ id: 'report-tests', icon: 'fa-flask', labelKey: 'testsReportTitle' },
|
||||
@@ -538,6 +541,8 @@ const SELF_LOADING_PANES = [
|
||||
'report-security', 'report-speed', 'report-tests', 'report-cpu', 'report-ram', 'report-disk',
|
||||
'report-instrumentation',
|
||||
'report-database', 'report-integrity', 'report-office', 'report-api', 'report-mail',
|
||||
// passwordLengthReport.js loads its own rows and settings through methods.
|
||||
'report-password-length',
|
||||
];
|
||||
|
||||
function openReportPane(tmpl, targetID) {
|
||||
|
||||
@@ -281,6 +281,17 @@ template(name="attachments")
|
||||
.materialCheckBox#s3-force-path-style(class="{{#if cloudValue.s3.forcePathStyle}}is-checked{{/if}}")
|
||||
span {{_ 's3-force-path-style'}}
|
||||
small.form-text.text-muted {{_ 's3-force-path-style-description'}}
|
||||
//- Encryption at rest (models/lib/s3Encryption.js).
|
||||
.form-group
|
||||
label(for="s3-sse") {{_ 's3-sse'}}
|
||||
select.wekan-form-control#s3-sse
|
||||
option(value="" selected="{{s3SseSelected ''}}") {{_ 's3-sse-none'}}
|
||||
option(value="AES256" selected="{{s3SseSelected 'AES256'}}") SSE-S3 (AES256)
|
||||
option(value="aws:kms" selected="{{s3SseSelected 'aws:kms'}}") SSE-KMS (aws:kms)
|
||||
small.form-text.text-muted {{_ 's3-sse-description'}}
|
||||
.form-group
|
||||
label(for="s3-sse-kms-key-id") {{_ 's3-sse-kms-key-id'}}
|
||||
input.wekan-form-control#s3-sse-kms-key-id(type="text" value="{{cloudValue.s3.sseKmsKeyId}}" placeholder="arn:aws:kms:eu-north-1:111122223333:key/…")
|
||||
|
||||
.form-group
|
||||
button.js-test-cloud-connection.btn.btn-secondary(data-provider="s3") {{_ 'test-cloud-connection'}}
|
||||
|
||||
@@ -32,6 +32,8 @@ const CLOUD_FIELD_IDS = {
|
||||
accessKeyId: '#s3-access-key',
|
||||
secretAccessKey: '#s3-secret-key',
|
||||
forcePathStyle: '#s3-force-path-style',
|
||||
serverSideEncryption: '#s3-sse',
|
||||
sseKmsKeyId: '#s3-sse-kms-key-id',
|
||||
},
|
||||
azure: {
|
||||
enabled: '#azure-enabled',
|
||||
@@ -858,6 +860,11 @@ Template.attachments.helpers({
|
||||
CLOUD_PROVIDERS.forEach(p => { out[p] = cloudConfigFromSettings(tpl, p).read !== false; });
|
||||
return out;
|
||||
},
|
||||
// The S3 server-side encryption option that is stored (models/lib/s3Encryption.js).
|
||||
s3SseSelected(mode) {
|
||||
const stored = cloudConfigFromSettings(Template.instance(), 's3').serverSideEncryption || '';
|
||||
return stored === mode ? 'selected' : null;
|
||||
},
|
||||
cloudValue() {
|
||||
const tpl = Template.instance();
|
||||
const s3 = cloudConfigFromSettings(tpl, 's3');
|
||||
@@ -872,6 +879,8 @@ Template.attachments.helpers({
|
||||
bucket: s3.bucket || '',
|
||||
accessKeyId: s3.accessKeyId || '',
|
||||
forcePathStyle: s3.forcePathStyle !== false,
|
||||
serverSideEncryption: s3.serverSideEncryption || '',
|
||||
sseKmsKeyId: s3.sseKmsKeyId || '',
|
||||
},
|
||||
azure: {
|
||||
accountName: azure.accountName || '',
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//- Admin Panel / Problems / Password length (docs/Features/Admin-Panel/Problems/Password-length.md):
|
||||
//- the minimum, whether it is enforced, and who signs in with a shorter
|
||||
//- password - through the shared table page (docs/Features/General/Table.md).
|
||||
template(name="passwordLengthReport")
|
||||
ul.setting-detail.password-length-settings
|
||||
li
|
||||
label(for="password-min-length-input") {{_ 'password-min-length'}}
|
||||
input#password-min-length-input.js-password-min-length(type="number" min="1" max="256" value="{{minLength}}")
|
||||
p.description {{_ 'password-min-length-description'}}
|
||||
li
|
||||
a.flex.js-toggle-password-min-length-enforce
|
||||
.materialCheckBox(class="{{#if enforce}}is-checked{{/if}}")
|
||||
span {{_ 'password-min-length-enforce'}}
|
||||
p.description {{_ 'password-min-length-enforce-description'}}
|
||||
li
|
||||
button.primary.js-save-password-length(type="button") {{_ 'save'}}
|
||||
if saved
|
||||
span.password-length-saved {{_ 'password-length-saved'}}
|
||||
if saveError
|
||||
p.error.password-length-error= saveError
|
||||
if fromEnvironment
|
||||
p.description {{_ 'password-length-from-environment'}}
|
||||
+tablePage(tablePageData)
|
||||
@@ -0,0 +1,157 @@
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { formatDateForDisplay } from '/client/lib/dateDisplay';
|
||||
import { buildFilters, buildHeader, buildRows, pageInfo } from '/models/lib/tablePage';
|
||||
import { errorMessage } from '/client/lib/errorMessage';
|
||||
|
||||
// Admin Panel / Problems / Password length: the two settings and the table of
|
||||
// accounts that sign in with a password shorter than the minimum
|
||||
// (server/lib/shortPasswordLogins.js; docs/Features/Admin-Panel/Problems/Password-length.md).
|
||||
const { addressValue, locationText, locationFlag } = require('/models/lib/addressReportColumns');
|
||||
const { DEFAULT_FILTER } = require('/models/lib/shortPasswordLogins');
|
||||
|
||||
const PER_PAGE = 25;
|
||||
const formatDate = date => (date ? formatDateForDisplay(date) : '');
|
||||
|
||||
// One cell per family: the address, then the city when a CDN in front of
|
||||
// WeKan said where it is, and the country's flag before both.
|
||||
const addressColumn = (labelKey, family) => ({
|
||||
labelKey,
|
||||
nowrap: true,
|
||||
value: row => [addressValue(row, family), locationText(row, family)].filter(Boolean).join(' '),
|
||||
flag: row => locationFlag(row, family),
|
||||
});
|
||||
|
||||
const COLUMNS = [
|
||||
{ labelKey: 'username', value: row => row.username || row.userId },
|
||||
{ labelKey: 'password-length-first-login', nowrap: true, value: row => formatDate(row.firstAt) },
|
||||
addressColumn('event-ipv4', 'ipv4'),
|
||||
addressColumn('event-ipv6', 'ipv6'),
|
||||
{ labelKey: 'password-length-last-login', nowrap: true, value: row => formatDate(row.at) },
|
||||
{ labelKey: 'password-length-login-count', align: 'end', value: row => row.count || 0 },
|
||||
{ labelKey: 'password-length-fixed', nowrap: true, value: row => formatDate(row.fixedAt) },
|
||||
];
|
||||
|
||||
const FILTER_OPTIONS = [
|
||||
{ value: 'short', labelKey: 'password-length-filter-short' },
|
||||
{ value: 'fixed', labelKey: 'password-length-filter-fixed' },
|
||||
{ value: 'all', labelKey: 'password-length-filter-all' },
|
||||
];
|
||||
|
||||
Template.passwordLengthReport.onCreated(function () {
|
||||
this.rows = new ReactiveVar([]);
|
||||
this.total = new ReactiveVar(0);
|
||||
this.page = new ReactiveVar(1);
|
||||
this.search = new ReactiveVar('');
|
||||
this.filter = new ReactiveVar(DEFAULT_FILTER);
|
||||
this.sources = new ReactiveVar(null);
|
||||
// The working copy of the two settings, until Save.
|
||||
this.minLength = new ReactiveVar(8);
|
||||
this.enforce = new ReactiveVar(false);
|
||||
this.saved = new ReactiveVar(false);
|
||||
this.error = new ReactiveVar('');
|
||||
|
||||
this.load = () => {
|
||||
Meteor.call('shortPasswordLoginsPage', {
|
||||
filter: this.filter.get(),
|
||||
search: this.search.get() || undefined,
|
||||
limit: PER_PAGE,
|
||||
skip: (this.page.get() - 1) * PER_PAGE,
|
||||
}, (error, result) => {
|
||||
if (error) return;
|
||||
this.rows.set((result && result.rows) || []);
|
||||
this.total.set((result && result.total) || 0);
|
||||
});
|
||||
};
|
||||
this.loadSettings = () => {
|
||||
Meteor.call('getAuthConfigSources', 'login', (error, result) => {
|
||||
if (error || !result) return;
|
||||
this.sources.set(result.sources || {});
|
||||
this.minLength.set(result.sources?.passwordMinLength?.value || 8);
|
||||
this.enforce.set(result.sources?.passwordMinLengthEnforce?.value === true);
|
||||
});
|
||||
};
|
||||
this.load();
|
||||
this.loadSettings();
|
||||
});
|
||||
|
||||
Template.passwordLengthReport.helpers({
|
||||
minLength() { return Template.instance().minLength.get(); },
|
||||
enforce() { return Template.instance().enforce.get(); },
|
||||
saved() { return Template.instance().saved.get(); },
|
||||
saveError() { return Template.instance().error.get(); },
|
||||
// An environment variable set on the host is overridden by a save here; say
|
||||
// so, rather than let the admin wonder which one is in effect.
|
||||
fromEnvironment() {
|
||||
const sources = Template.instance().sources.get() || {};
|
||||
return ['passwordMinLength', 'passwordMinLengthEnforce'].some(key => sources[key]?.source === 'env');
|
||||
},
|
||||
tablePageData() {
|
||||
const t = Template.instance();
|
||||
const rows = t.rows.get() || [];
|
||||
const info = pageInfo(t.total.get(), t.page.get(), PER_PAGE);
|
||||
return {
|
||||
descKey: 'password-length-report-desc',
|
||||
emptyKey: 'password-length-no-results',
|
||||
searchTerm: t.search.get(),
|
||||
filters: buildFilters([{
|
||||
id: 'password-length-status', labelKey: 'status', current: t.filter.get(), options: FILTER_OPTIONS,
|
||||
}]),
|
||||
header: buildHeader(COLUMNS),
|
||||
rows: buildRows(rows, COLUMNS),
|
||||
rowCount: rows.length,
|
||||
totalCount: t.total.get(),
|
||||
page: info.page,
|
||||
totalPages: info.totalPages,
|
||||
hasPrev: info.hasPrev,
|
||||
hasNext: info.hasNext,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Template.passwordLengthReport.events({
|
||||
'input .js-password-min-length'(event, tmpl) {
|
||||
tmpl.minLength.set(parseInt(event.currentTarget.value, 10) || '');
|
||||
tmpl.saved.set(false);
|
||||
},
|
||||
'click .js-toggle-password-min-length-enforce'(event, tmpl) {
|
||||
event.preventDefault();
|
||||
tmpl.enforce.set(!tmpl.enforce.get());
|
||||
tmpl.saved.set(false);
|
||||
},
|
||||
'click .js-save-password-length'(event, tmpl) {
|
||||
event.preventDefault();
|
||||
Meteor.call('saveAuthConfigSettings', 'login', {
|
||||
passwordMinLength: tmpl.minLength.get() || null,
|
||||
passwordMinLengthEnforce: tmpl.enforce.get(),
|
||||
}, error => {
|
||||
tmpl.error.set(error ? errorMessage(error) : '');
|
||||
if (error) return;
|
||||
tmpl.saved.set(true);
|
||||
tmpl.loadSettings();
|
||||
});
|
||||
},
|
||||
'change .js-table-page-filter'(event, tmpl) {
|
||||
tmpl.filter.set(event.currentTarget.value || DEFAULT_FILTER);
|
||||
tmpl.page.set(1);
|
||||
tmpl.load();
|
||||
},
|
||||
'click .js-table-page-prev'(event, tmpl) {
|
||||
event.preventDefault();
|
||||
if (tmpl.page.get() > 1) { tmpl.page.set(tmpl.page.get() - 1); tmpl.load(); }
|
||||
},
|
||||
'click .js-table-page-next'(event, tmpl) {
|
||||
event.preventDefault();
|
||||
const info = pageInfo(tmpl.total.get(), tmpl.page.get(), PER_PAGE);
|
||||
if (info.hasNext) { tmpl.page.set(tmpl.page.get() + 1); tmpl.load(); }
|
||||
},
|
||||
'keydown .js-table-page-search'(event, tmpl) {
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
tmpl.search.set(event.currentTarget.value || '');
|
||||
tmpl.page.set(1);
|
||||
tmpl.load();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -36,6 +36,8 @@ template(name="people")
|
||||
+authProviderSettings(loginOptionsData)
|
||||
//- Windows SSO, which works only on a Windows server.
|
||||
+authProviderSettings(windowsSsoData)
|
||||
//- Strict-Transport-Security, sent when ROOT_URL is https.
|
||||
+authProviderSettings(httpSecurityData)
|
||||
else if samlSetting
|
||||
+authProviderSettings(samlSettingsData)
|
||||
else if authConfigSection
|
||||
@@ -56,6 +58,8 @@ template(name="people")
|
||||
+peopleGeneral(peopleTablePageData)
|
||||
else if lockedUsersSetting.get
|
||||
+lockedUsersGeneral
|
||||
else if auditLogSetting.get
|
||||
+adminAuditLog
|
||||
else if rolesSetting.get
|
||||
+rolesGeneral
|
||||
else if templatesSetting.get
|
||||
|
||||
@@ -142,6 +142,7 @@ Template.people.onCreated(function () {
|
||||
this.teamSetting = new ReactiveVar(false);
|
||||
this.peopleSetting = new ReactiveVar(false);
|
||||
this.lockedUsersSetting = new ReactiveVar(false);
|
||||
this.auditLogSetting = new ReactiveVar(false);
|
||||
this.rolesSetting = new ReactiveVar(false);
|
||||
this.templatesSetting = new ReactiveVar(false);
|
||||
// #5850: Admin Panel > People > Domains tab. The domains table itself
|
||||
@@ -434,6 +435,7 @@ Template.people.onCreated(function () {
|
||||
this.teamSetting.set('team-setting' === targetID);
|
||||
this.peopleSetting.set('people-setting' === targetID);
|
||||
this.lockedUsersSetting.set('locked-users-setting' === targetID);
|
||||
this.auditLogSetting.set('audit-log-setting' === targetID);
|
||||
this.rolesSetting.set('roles-setting' === targetID);
|
||||
this.templatesSetting.set('templates-setting' === targetID);
|
||||
this.domainSetting.set('domains-setting' === targetID);
|
||||
@@ -551,6 +553,9 @@ function peopleMenu(user) {
|
||||
{ id: 'team-setting', icon: 'fa-users', labelKey: 'teams' },
|
||||
{ id: 'people-setting', icon: 'fa-user', labelKey: 'people' },
|
||||
{ id: 'locked-users-setting', icon: 'fa-lock', labelKey: 'accounts-lockout-locked-users', iconWrapCls: 'text-red' },
|
||||
// What site administrators did (models/lib/adminAudit.js). Site admins only:
|
||||
// tenantAdminPeopleMenu keeps a per-tenant admin's menu to its own panes.
|
||||
{ id: 'audit-log-setting', icon: 'fa-list-alt', labelKey: 'admin-audit-log' },
|
||||
{ id: 'roles-setting', icon: 'fa-key', labelKey: 'roles' },
|
||||
{ id: 'templates-setting', icon: 'fa-clone', labelKey: 'shared-templates' },
|
||||
{ id: 'registration-setting', icon: 'fa-key', labelKey: 'login', emoji: true },
|
||||
@@ -581,6 +586,15 @@ function peopleMenu(user) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// A password the server refused (server/lib/passwordPolicy.js), in the
|
||||
// reader's language; the popup that sent it has already closed.
|
||||
function passwordErrorMessage(error) {
|
||||
const key = error && error.error;
|
||||
if (key === 'password-too-short') return TAPi18n.__('password-too-short', { min: error.details?.min });
|
||||
if (['password-pwned', 'password-same-as-username'].includes(key)) return TAPi18n.__(key);
|
||||
return error.reason || error.message;
|
||||
}
|
||||
|
||||
// People panes drawn from models/lib/authConfigCatalog.js, and the data the
|
||||
// catalog form (authProviderSettings) needs for one section.
|
||||
const AUTH_CONFIG_PANES = {
|
||||
@@ -899,6 +913,8 @@ Template.people.helpers({
|
||||
// The Login pane's own login settings (PASSWORD_LOGIN_ENABLED, ...).
|
||||
loginOptionsData() { return authConfigData('login'); },
|
||||
windowsSsoData() { return authConfigData('windowsSso'); },
|
||||
// HSTS (HSTS_MAX_AGE, ...), below the login settings.
|
||||
httpSecurityData() { return authConfigData('httpSecurity'); },
|
||||
ldapPane() { return Template.instance().activeMenuId.get() === 'ldap-setting'; },
|
||||
authenticationSection() {
|
||||
return {
|
||||
@@ -927,6 +943,9 @@ Template.people.helpers({
|
||||
lockedUsersSetting() {
|
||||
return Template.instance().lockedUsersSetting;
|
||||
},
|
||||
auditLogSetting() {
|
||||
return Template.instance().auditLogSetting;
|
||||
},
|
||||
rolesSetting() {
|
||||
return Template.instance().rolesSetting;
|
||||
},
|
||||
@@ -2352,7 +2371,9 @@ Template.editUserPopup.events({
|
||||
});
|
||||
|
||||
if (isChangePassword) {
|
||||
Meteor.call('setPassword', password, this.userId);
|
||||
Meteor.call('setPassword', password, this.userId, error => {
|
||||
if (error) alert(passwordErrorMessage(error));
|
||||
});
|
||||
}
|
||||
|
||||
if (isChangeEmailVerified) {
|
||||
@@ -2651,6 +2672,8 @@ Template.newUserPopup.events({
|
||||
} else if (errorElement === 'email-already-taken') {
|
||||
usernameMessageElement.hide();
|
||||
emailMessageElement.show();
|
||||
} else if (String(errorElement).startsWith('password-')) {
|
||||
alert(passwordErrorMessage(error));
|
||||
}
|
||||
} else {
|
||||
usernameMessageElement.hide();
|
||||
|
||||
@@ -750,6 +750,12 @@ body.sidebar-resizing-active {
|
||||
margin-inline-end: 6px;
|
||||
appearance: auto;
|
||||
}
|
||||
/* Why a date range was refused, below its buttons (sidebarFilters.js,
|
||||
rangeRefused): the browser's own bubble covered those buttons in Firefox. */
|
||||
.filter-date-range-error {
|
||||
color: #c0392b;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
.filter-native-group {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -140,7 +140,10 @@ template(name="filterSidebar")
|
||||
| {{_ 'filter-dates-label' }}
|
||||
each dateFilterProviderViews
|
||||
+Template.dynamic(template=template data=data)
|
||||
form.js-card-movement-range
|
||||
//- novalidate: the range is checked in the submit handler (rangeRefused in
|
||||
sidebarFilters.js). WebKit keeps a half-edited date segment after Clear
|
||||
empties a field and then refused every submit of the form, with no message.
|
||||
form.js-card-movement-range(novalidate)
|
||||
h4 {{_ 'filter-movement-range'}}
|
||||
label(for="filter-card-movement-from") {{_ 'filter-date-range-from'}}
|
||||
input.js-card-movement-from(id="filter-card-movement-from" type="date" value=movementFrom)
|
||||
@@ -148,7 +151,9 @@ template(name="filterSidebar")
|
||||
input.js-card-movement-to(id="filter-card-movement-to" type="date" value=movementTo)
|
||||
button.primary(type="submit") {{_ 'apply'}}
|
||||
button.js-clear-movement-range(type="button") {{_ 'filter-clear'}}
|
||||
form.js-card-date-range
|
||||
if movementRangeError
|
||||
p.js-card-movement-range-error.filter-date-range-error(role="alert") {{_ 'filter-date-range-invalid'}}
|
||||
form.js-card-date-range(novalidate)
|
||||
label(for="filter-card-date-field") {{_ 'filter-date-range-field'}}
|
||||
select.js-card-date-field(id="filter-card-date-field")
|
||||
each dateRangeFields
|
||||
@@ -162,6 +167,8 @@ template(name="filterSidebar")
|
||||
| {{_ 'filter-date-range-missing'}}
|
||||
button.primary(type="submit") {{_ 'apply'}}
|
||||
button.js-clear-date-range(type="button") {{_ 'filter-clear'}}
|
||||
if dateRangeError
|
||||
p.js-card-date-range-error.filter-date-range-error(role="alert") {{_ 'filter-date-range-invalid'}}
|
||||
hr
|
||||
fieldset.filter-native-group
|
||||
legend {{_ 'due-date'}}
|
||||
|
||||
@@ -13,9 +13,23 @@ import { getSidebarInstance } from '/client/features/sidebar/service';
|
||||
import { CARD_DATE_RANGE_FIELDS, CARD_RECENCY_PRESETS } from '/models/lib/cardDateRange';
|
||||
import { DEPENDENCY_TYPES } from '/models/metadata/dependencies';
|
||||
|
||||
// A refused date range marks its end field invalid - so it is :invalid and
|
||||
// screen readers announce the reason - and says why in the form itself. Not
|
||||
// with reportValidity(): Firefox's validation bubble opens over the Apply and
|
||||
// Clear buttons below the field and takes every click on them while the field
|
||||
// has focus, so the range could not be cleared at all.
|
||||
function rangeRefused(end, error, refused) {
|
||||
end.setCustomValidity(refused ? TAPi18n.__('filter-date-range-invalid') : '');
|
||||
error.set(refused);
|
||||
}
|
||||
|
||||
Template.filterSidebar.onCreated(function () {
|
||||
this.presetMessage = new ReactiveVar('');
|
||||
this.presetBusy = new ReactiveVar(false);
|
||||
// Why a date range was refused, shown in the form rather than in the
|
||||
// browser's own validation bubble (see rangeRefused below).
|
||||
this.movementRangeError = new ReactiveVar(false);
|
||||
this.dateRangeError = new ReactiveVar(false);
|
||||
this.autorun(() => {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (Meteor.userId() && boardId) this.subscribe('savedCardFilters', boardId);
|
||||
@@ -28,6 +42,8 @@ Template.filterSidebar.helpers({
|
||||
},
|
||||
presetMessage() { return Template.instance().presetMessage.get(); },
|
||||
presetBusy() { return Template.instance().presetBusy.get(); },
|
||||
movementRangeError() { return Template.instance().movementRangeError.get(); },
|
||||
dateRangeError() { return Template.instance().dateRangeError.get(); },
|
||||
filterProviderViews() { return Filter.providers.views(); },
|
||||
dateFilterProviderViews() { return Filter.providers.views('dates'); },
|
||||
dueUnrestricted() { return Filter.dueAt.state() === null; },
|
||||
@@ -102,15 +118,15 @@ Template.filterSidebar.events({
|
||||
const end = tpl.find('.js-card-movement-to');
|
||||
const valid = Filter.movementDate.set({ field: 'createdAt',
|
||||
from: tpl.find('.js-card-movement-from').value, to: end.value, includeMissing: false });
|
||||
end.setCustomValidity(valid ? '' : TAPi18n.__('filter-date-range-invalid'));
|
||||
if (!valid) end.reportValidity();
|
||||
else Filter.resetExceptions();
|
||||
rangeRefused(end, tpl.movementRangeError, !valid);
|
||||
if (valid) Filter.resetExceptions();
|
||||
},
|
||||
'input .js-card-movement-range input'(event, tpl) {
|
||||
tpl.find('.js-card-movement-to').setCustomValidity('');
|
||||
rangeRefused(tpl.find('.js-card-movement-to'), tpl.movementRangeError, false);
|
||||
},
|
||||
'click .js-clear-movement-range'(event, tpl) {
|
||||
event.preventDefault(); tpl.find('.js-card-movement-to').setCustomValidity('');
|
||||
event.preventDefault();
|
||||
rangeRefused(tpl.find('.js-card-movement-to'), tpl.movementRangeError, false);
|
||||
Filter.movementDate.reset(); Filter.resetExceptions();
|
||||
},
|
||||
'submit .js-card-date-range'(event, tpl) {
|
||||
@@ -119,22 +135,20 @@ Template.filterSidebar.events({
|
||||
const valid = Filter.dateRange.set({ field: tpl.find('.js-card-date-field').value,
|
||||
from: tpl.find('.js-card-date-from').value, to: end.value,
|
||||
includeMissing: tpl.find('.js-card-date-missing').checked });
|
||||
end.setCustomValidity(valid ? '' : TAPi18n.__('filter-date-range-invalid'));
|
||||
if (!valid) end.reportValidity();
|
||||
else Filter.resetExceptions();
|
||||
rangeRefused(end, tpl.dateRangeError, !valid);
|
||||
if (valid) Filter.resetExceptions();
|
||||
},
|
||||
'input .js-card-date-range input'(event, tpl) {
|
||||
tpl.find('.js-card-date-to').setCustomValidity('');
|
||||
rangeRefused(tpl.find('.js-card-date-to'), tpl.dateRangeError, false);
|
||||
},
|
||||
'click .js-clear-date-range'(event, tpl) {
|
||||
event.preventDefault();
|
||||
// Reset the native date controls as well as the reactive model. WebKit can
|
||||
// retain an edited date segment after Blaze changes the value attribute.
|
||||
for (const selector of ['.js-card-date-from', '.js-card-date-to']) {
|
||||
const input = tpl.find(selector);
|
||||
input.value = '';
|
||||
input.setCustomValidity('');
|
||||
tpl.find(selector).value = '';
|
||||
}
|
||||
rangeRefused(tpl.find('.js-card-date-to'), tpl.dateRangeError, false);
|
||||
Filter.dateRange.reset(); Filter.resetExceptions();
|
||||
},
|
||||
'submit .js-column-age-filter'(event, tpl) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/* PASSWORD_MIN_LENGTH: the Change Password form over everything, until done. */
|
||||
.password-change-required {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.password-change-required-box {
|
||||
max-width: 32rem;
|
||||
width: calc(100% - 2rem);
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
}
|
||||
.password-change-required-box h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
/* layouts.css hides the password form until a page shows it (#6380). */
|
||||
.password-change-required .at-pwd-form {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
template(name="passwordChangeRequired")
|
||||
//- PASSWORD_MIN_LENGTH (server/passwordChangeRequired.js): an account whose
|
||||
//- password is shorter than the minimum signed in, and sees only this until
|
||||
//- it has a longer one. The form is the same Change Password Member
|
||||
//- Settings opens; changing it signs every session out, this one too.
|
||||
.password-change-required(role="dialog" aria-modal="true" aria-labelledby="password-change-required-title")
|
||||
.password-change-required-box
|
||||
h2#password-change-required-title {{_ 'password-change-required-title'}}
|
||||
p {{_ 'password-change-required-text' min=min}}
|
||||
+atForm(state='changePwd')
|
||||
.buttonsContainer
|
||||
button.js-password-change-required-logout(type="button") {{_ 'log-out'}}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
|
||||
// PASSWORD_MIN_LENGTH: after signing in, ask whether this account must change
|
||||
// a password shorter than the minimum, and if so show nothing but the Change
|
||||
// Password form (passwordChangeRequired.jade). The server asks the question
|
||||
// (server/passwordChangeRequired.js); config/accounts.js tells it when the
|
||||
// password just typed was too short. Changing a password signs every session
|
||||
// out, so the account then signs in again, with the new password.
|
||||
let overlay = null;
|
||||
|
||||
function closeOverlay() {
|
||||
if (!overlay) return;
|
||||
Blaze.remove(overlay.view);
|
||||
overlay.node.remove();
|
||||
overlay = null;
|
||||
}
|
||||
|
||||
function openOverlay(min) {
|
||||
if (overlay) return;
|
||||
const node = document.createElement('div');
|
||||
document.body.appendChild(node);
|
||||
overlay = { node, view: Blaze.renderWithData(Template.passwordChangeRequired, { min }, node) };
|
||||
}
|
||||
|
||||
Template.passwordChangeRequired.events({
|
||||
'click .js-password-change-required-logout'() {
|
||||
Meteor.logout(() => closeOverlay());
|
||||
},
|
||||
});
|
||||
|
||||
export function checkPasswordChangeRequirement() {
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) return;
|
||||
Meteor.call('getPasswordChangeRequirement', (error, result) => {
|
||||
if (error || !result || Meteor.userId() !== userId) return;
|
||||
if (result.required) openOverlay(result.minLength);
|
||||
});
|
||||
}
|
||||
|
||||
// A changed password signs this session out too (server/passwordChangeRequired.js).
|
||||
// The server closes the connection as it removes the token, so the answer to
|
||||
// the change - and useraccounts' onSubmitHook - may never arrive; being signed
|
||||
// out soon after submitting a change is the answer, and the next stop is the
|
||||
// sign-in page, from Member Settings and from this overlay alike.
|
||||
const SIGN_OUT_WAIT_MS = 2 * 60 * 1000;
|
||||
let changeSubmittedAt = 0;
|
||||
document.addEventListener('submit', event => {
|
||||
const form = event.target;
|
||||
if (form && form.id === 'at-pwd-form' && form.querySelector('#at-field-current_password')) {
|
||||
changeSubmittedAt = Date.now();
|
||||
}
|
||||
}, true);
|
||||
|
||||
Meteor.startup(() => {
|
||||
let askedFor = null;
|
||||
Tracker.autorun(() => {
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
askedFor = null;
|
||||
const changed = Date.now() - changeSubmittedAt < SIGN_OUT_WAIT_MS;
|
||||
if (overlay || changed) {
|
||||
// The change signed every session out: sign in with the new password.
|
||||
changeSubmittedAt = 0;
|
||||
closeOverlay();
|
||||
Popup.close?.();
|
||||
FlowRouter.go('atSignIn');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (askedFor === userId) return;
|
||||
askedFor = userId;
|
||||
Tracker.nonreactive(checkPasswordChangeRequirement);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/* REQUIRE_2FA: the set-up over everything, until it is done. */
|
||||
.two-factor-required {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.two-factor-required-box {
|
||||
max-width: 32rem;
|
||||
width: calc(100% - 2rem);
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
}
|
||||
.two-factor-required-box h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
template(name="twoFactorRequired")
|
||||
//- REQUIRE_2FA (models/lib/twoFactorRequirement.js): an account that must
|
||||
//- have two-factor authentication sees only this until it has set it up.
|
||||
//- The server refuses everything else meanwhile (server/requireTwoFactor.js);
|
||||
//- this is what the account can do about it. The set-up is the same
|
||||
//- twoFactorAuthPopup Member Settings opens.
|
||||
.two-factor-required(role="dialog" aria-modal="true" aria-labelledby="two-factor-required-title")
|
||||
.two-factor-required-box
|
||||
h2#two-factor-required-title {{_ 'two-factor-required-title'}}
|
||||
p {{_ 'two-factor-required-text'}}
|
||||
+twoFactorAuthPopup
|
||||
.buttonsContainer
|
||||
button.js-two-factor-required-logout(type="button") {{_ 'log-out'}}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
|
||||
// REQUIRE_2FA: after signing in, ask whether this account must still set up
|
||||
// two-factor authentication, and if so show nothing but the set-up
|
||||
// (twoFactorRequired.jade). accounts-2fa publishes the account's own
|
||||
// services.twoFactorAuthentication.type, so finishing the set-up is seen at
|
||||
// once; the page then reloads, because every subscription the server left
|
||||
// empty meanwhile has to be made again.
|
||||
let overlay = null;
|
||||
|
||||
function closeOverlay() {
|
||||
if (!overlay) return;
|
||||
Blaze.remove(overlay.view);
|
||||
overlay.node.remove();
|
||||
overlay = null;
|
||||
}
|
||||
|
||||
function openOverlay() {
|
||||
if (overlay) return;
|
||||
const node = document.createElement('div');
|
||||
document.body.appendChild(node);
|
||||
overlay = { node, view: Blaze.render(Template.twoFactorRequired, node) };
|
||||
}
|
||||
|
||||
Template.twoFactorRequired.events({
|
||||
'click .js-two-factor-required-logout'() {
|
||||
Meteor.logout(() => closeOverlay());
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.startup(() => {
|
||||
let askedFor = null;
|
||||
Tracker.autorun(() => {
|
||||
const userId = Meteor.userId();
|
||||
const user = Meteor.user({ fields: { 'services.twoFactorAuthentication.type': 1 } });
|
||||
if (!userId) {
|
||||
askedFor = null;
|
||||
closeOverlay();
|
||||
return;
|
||||
}
|
||||
const enabled = user?.services?.twoFactorAuthentication?.type === 'otp';
|
||||
if (overlay && enabled) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
if (askedFor === userId) return;
|
||||
askedFor = userId;
|
||||
Meteor.call('getTwoFactorRequirement', (error, result) => {
|
||||
if (error || !result || Meteor.userId() !== userId) return;
|
||||
if (result.required && !result.enabled) openOverlay();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -204,3 +204,57 @@ body.mobile-mode.userform-layout {
|
||||
#at-meteor-developer, #at-weibo, #at-meetup) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* #6835: Member Settings / Change Settings. Its rows are the shared popup list
|
||||
(popup.css: a flex row, every line 33px high, the icon centred), which reads
|
||||
well for one-line items and badly for these: a setting whose words wrap -
|
||||
most of them, in Persian and German - became two 33px lines with its icon
|
||||
floating between them and the next setting right below, with nothing to
|
||||
tell one from the other. Here a row is an icon column, the words, and the
|
||||
check mark at the end; the icon sits on the first line of the words, a
|
||||
wrapped line is an ordinary line high, and a rule divides the rows. Scoped
|
||||
to the list's own class: the member menu before it shares its .pop-over. */
|
||||
.pop-over-list.change-settings-list li + li {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.pop-over-list.change-settings-list li > a {
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
line-height: 1.35;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.pop-over-list.change-settings-list li > a > i.fa:first-child {
|
||||
flex: 0 0 1.4em;
|
||||
margin-inline-end: 0.6em;
|
||||
line-height: inherit;
|
||||
text-align: center;
|
||||
}
|
||||
.pop-over-list.change-settings-list li > a > i.fa-check {
|
||||
flex: 0 0 auto;
|
||||
margin-inline-start: auto;
|
||||
padding-inline-start: 0.6em;
|
||||
line-height: inherit;
|
||||
}
|
||||
.pop-over-list.change-settings-list li > label.bold {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
line-height: 1.35;
|
||||
padding: 8px 10px 4px;
|
||||
}
|
||||
.pop-over-list.change-settings-list li > label.bold > i.fa {
|
||||
flex: 0 0 1.4em;
|
||||
margin-inline-end: 0.6em;
|
||||
text-align: center;
|
||||
line-height: inherit;
|
||||
}
|
||||
/* The Card settings checkbox row: the box on the first line of its words,
|
||||
with the same space between them as an icon has. */
|
||||
.pop-over-list.change-settings-list .js-rescue-card-description {
|
||||
align-items: flex-start;
|
||||
gap: 0.6em;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.pop-over-list.change-settings-list .js-rescue-card-description .materialCheckBox {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 0.15em;
|
||||
}
|
||||
@@ -410,7 +410,10 @@ template(name="changeFontPopup")
|
||||
a.js-reset-text-color(class="{{#unless hasTextColor}}disabled{{/unless}}") {{_ 'remove-btn'}}
|
||||
|
||||
template(name="changeSettingsPopup")
|
||||
ul.pop-over-list
|
||||
//- #6835: its own class - a popup keeps the screens before it (the member
|
||||
menu) side by side in the same .pop-over, so the popup's data-popup would
|
||||
restyle those too. userForm.css
|
||||
ul.pop-over-list.change-settings-list
|
||||
li
|
||||
a.js-toggle-desktop-drag-handles
|
||||
i.fa.fa-arrows
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '/client/components/settings/adminProblems.jade';
|
||||
import '/client/components/settings/adminAuditLog.jade';
|
||||
import '/client/components/settings/attachments.jade';
|
||||
// Backup status and history, shared by Attachments / Backup and Problems / Backup.
|
||||
import '/client/components/settings/backupRunsStatus.jade';
|
||||
@@ -33,6 +34,7 @@ import '/client/components/settings/invitationCode.js';
|
||||
// the `collapsed` class either. docs/Features/All-Boards/Left-Menu.md
|
||||
import '/client/components/settings/leftMenu.js';
|
||||
import '/client/components/settings/lockedUsersBody.js';
|
||||
import '/client/components/settings/adminAuditLog.js';
|
||||
import '/client/components/settings/migrationProgress.js';
|
||||
import '/client/components/settings/notificationSettingsPopup.js';
|
||||
// #3695: notification delivery (content, grouping, schedule) per channel, in
|
||||
@@ -48,6 +50,8 @@ import '/client/components/settings/sharedLabelsSettings.js';
|
||||
import '/client/components/settings/settingHeader.js';
|
||||
import '/client/components/settings/problemsSummary.jade';
|
||||
import '/client/components/settings/problemsSummary.js';
|
||||
import '/client/components/settings/passwordLengthReport.jade';
|
||||
import '/client/components/settings/passwordLengthReport.js';
|
||||
import '/client/components/settings/translationBody.js';
|
||||
|
||||
// adminProblems.css was missing here, so NONE of the Admin Panel report styling
|
||||
@@ -59,6 +63,7 @@ import '/client/components/settings/adminProblems.css';
|
||||
import '/client/components/settings/attachments.css';
|
||||
import '/client/components/settings/informationBody.css';
|
||||
import '/client/components/settings/lockedUsersBody.css';
|
||||
import '/client/components/settings/adminAuditLog.css';
|
||||
import '/client/components/settings/migrationProgress.css';
|
||||
import '/client/components/settings/peopleBody.css';
|
||||
import '/client/components/settings/settingBody.css';
|
||||
|
||||
@@ -2,13 +2,19 @@ import '/client/components/users/passwordInput.jade';
|
||||
import '/client/components/users/userAvatar.jade';
|
||||
import '/client/components/users/userHeader.jade';
|
||||
import '/client/components/users/dependencyMenu.jade';
|
||||
import '/client/components/users/twoFactorRequired.jade';
|
||||
import '/client/components/users/passwordChangeRequired.jade';
|
||||
|
||||
import '/client/components/users/passwordInput.js';
|
||||
import '/client/components/users/userAvatar.js';
|
||||
import '/client/components/users/userHeader.js';
|
||||
import '/client/components/users/dependencyMenu.js';
|
||||
import '/client/components/users/twoFactorRequired.js';
|
||||
import '/client/components/users/passwordChangeRequired.js';
|
||||
|
||||
import '/client/components/users/userAvatar.css';
|
||||
import '/client/components/users/userForm.css';
|
||||
import '/client/components/users/dependencyMenu.css';
|
||||
import '/client/components/users/twoFactorRequired.css';
|
||||
import '/client/components/users/passwordChangeRequired.css';
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// LOGOUT_IDLE_MINUTES: tell the server, at most once a minute, that the user
|
||||
// did something here - a key press, a click, a touch, a scroll - so a session
|
||||
// in use is not signed out as idle (models/lib/idleLogout.js). Nothing is sent
|
||||
// while the page only sits open, and nothing at all when the limit is off.
|
||||
|
||||
const { TOUCH_INTERVAL_MS } = require('../../models/lib/idleLogout');
|
||||
|
||||
const EVENTS = ['keydown', 'mousedown', 'touchstart', 'wheel', 'scroll'];
|
||||
|
||||
// target: where to listen (document); touch(): report; now(): clock.
|
||||
function createIdleActivityReporter({ target, touch, now = () => Date.now(), interval = TOUCH_INTERVAL_MS }) {
|
||||
let last = 0;
|
||||
let listening = false;
|
||||
function onActivity() {
|
||||
const t = now();
|
||||
if (t - last < interval) return;
|
||||
last = t;
|
||||
touch();
|
||||
}
|
||||
return {
|
||||
start() {
|
||||
if (listening) return;
|
||||
listening = true;
|
||||
last = now(); // signing in was the activity that started this
|
||||
for (const name of EVENTS) target.addEventListener(name, onActivity, { capture: true, passive: true });
|
||||
},
|
||||
stop() {
|
||||
if (!listening) return;
|
||||
listening = false;
|
||||
for (const name of EVENTS) target.removeEventListener(name, onActivity, { capture: true });
|
||||
},
|
||||
onActivity,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createIdleActivityReporter, EVENTS };
|
||||
+36
-10
@@ -53,7 +53,20 @@ hotkeys.filter = (event) => {
|
||||
if (currentElement instanceof HTMLInputElement || currentElement instanceof HTMLSelectElement || currentElement instanceof HTMLTextAreaElement)
|
||||
return false;
|
||||
|
||||
if (currentElement.closest('button, a[href], summary, [role="button"], [role="tab"], [role="checkbox"]'))
|
||||
// A focused control keeps the keys it uses itself, and only those: Enter
|
||||
// activates any of them, Space a button, summary or checkbox (not a link),
|
||||
// and End, Home and the arrows move between tabs (A01). Every other key is
|
||||
// still a shortcut. Refusing ALL keys left shortcuts dead after a click on
|
||||
// a minicard - the minicard is a link, and clicking its date badge or its
|
||||
// menu leaves that link focused - and after any popup closed, because
|
||||
// closing returns focus to the button that opened it (#6755).
|
||||
const keysOwnedBy = {
|
||||
'button, a[href], summary, [role="button"], [role="tab"], [role="checkbox"]': [13],
|
||||
'button, summary, [role="button"], [role="checkbox"]': [32],
|
||||
'[role="tab"]': [35, 36, 37, 38, 39, 40],
|
||||
};
|
||||
if (Object.keys(keysOwnedBy).some(controls =>
|
||||
keysOwnedBy[controls].includes(event.keyCode) && currentElement.closest(controls)))
|
||||
return false;
|
||||
|
||||
// We can trigger events!
|
||||
@@ -404,10 +417,30 @@ function clickDueDateControl(cardId) {
|
||||
control.click();
|
||||
return true;
|
||||
}
|
||||
// #6840: the card's details render a moment AFTER the route names it - a few
|
||||
// hundred milliseconds on a slow server. Pressing `d` right after opening a
|
||||
// card found no control yet and did nothing, while the previous card's details
|
||||
// were still on screen. So the click waits until THIS card's control is there,
|
||||
// both for an opened card and for one `d` opens itself (#6755), and clicks it
|
||||
// once; it gives up quietly after 3 seconds (no due dates on this board).
|
||||
function clickDueDateControlWhenRendered(cardId) {
|
||||
const started = Date.now();
|
||||
const tryClick = () => {
|
||||
if (Utils.getCurrentCardId() !== cardId) return;
|
||||
if (clickDueDateControl(cardId)) return;
|
||||
if (Date.now() - started < 3000) setTimeout(tryClick, 50);
|
||||
};
|
||||
tryClick();
|
||||
}
|
||||
export function openDueDateEditorOfOpenedCard() {
|
||||
if (Meteor.userId() === null) return false;
|
||||
const cardId = Utils.getCurrentCardId();
|
||||
if (cardId) return clickDueDateControl(cardId);
|
||||
if (cardId) {
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
if (!card || !Utils.canModifyCard(card)) return false;
|
||||
clickDueDateControlWhenRendered(cardId);
|
||||
return true;
|
||||
}
|
||||
// #6755: with no card opened, `d` on a hovered or selected minicard - the
|
||||
// card every other card shortcut here acts on - opens that card and then
|
||||
// its Due date editor, rather than doing nothing until the card is opened.
|
||||
@@ -415,14 +448,7 @@ export function openDueDateEditorOfOpenedCard() {
|
||||
const minicard = minicardId && ReactiveCache.getCard(minicardId);
|
||||
if (!minicard || !Utils.canModifyCard(minicard)) return false;
|
||||
Utils.goCardId(minicardId);
|
||||
// The card details render after the route changes; wait for its control,
|
||||
// and give up quietly if it never comes (no due dates on this board).
|
||||
const started = Date.now();
|
||||
const tryClick = () => {
|
||||
if (Utils.getCurrentCardId() === minicardId && clickDueDateControl(minicardId)) return;
|
||||
if (Date.now() - started < 3000) setTimeout(tryClick, 50);
|
||||
};
|
||||
setTimeout(tryClick, 0);
|
||||
setTimeout(() => clickDueDateControlWhenRendered(minicardId), 0);
|
||||
return true;
|
||||
}
|
||||
hotkeys('d, shift+d', (event) => {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Breached-password check for the forms that set a NEW password - sign-up,
|
||||
// Change Password, reset and enrolment (models/lib/passwordPolicy.js says why
|
||||
// it is done in the browser). It captures the submit before useraccounts sees
|
||||
// it, asks the server for the SHA-1 range of the password - five hex digits,
|
||||
// never the password - and replays the submit when the password is not in it.
|
||||
//
|
||||
// A form that sets a new password is the one with a "password again" or a
|
||||
// "current password" field; the sign-in form has neither and is left alone.
|
||||
|
||||
const { isPwned, breachLookupDigest } = require('../../models/lib/passwordPolicy');
|
||||
|
||||
function isNewPasswordForm(form) {
|
||||
return !!(form && form.id === 'at-pwd-form' && form.querySelector('#at-field-password')
|
||||
&& (form.querySelector('#at-field-password_again') || form.querySelector('#at-field-current_password')));
|
||||
}
|
||||
|
||||
// policy() -> { checkPwned } (cached by the caller); range(prefix) -> Promise
|
||||
// of suffixes or null; subtle is crypto.subtle (absent outside a secure
|
||||
// context, where the check is skipped); showError(message); replay(form).
|
||||
function createNewPasswordSubmitHandler({ policy, range, subtle, showError, replay, pwnedMessage }) {
|
||||
let replaying = false;
|
||||
let pending = false;
|
||||
return async function submit(event) {
|
||||
const form = event.target;
|
||||
if (replaying || !isNewPasswordForm(form) || !policy().checkPwned || !subtle) return;
|
||||
const password = form.querySelector('#at-field-password').value;
|
||||
if (!password) return;
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
if (pending) return;
|
||||
pending = true;
|
||||
try {
|
||||
let breached = false;
|
||||
try {
|
||||
const hex = await breachLookupDigest(password, subtle);
|
||||
const suffixes = await range(hex.slice(0, 5));
|
||||
breached = Array.isArray(suffixes) && isPwned(hex, suffixes);
|
||||
} catch (e) {
|
||||
// An unreachable service must not stop anybody from setting a password.
|
||||
breached = false;
|
||||
}
|
||||
if (breached) {
|
||||
showError(pwnedMessage());
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
replaying = true;
|
||||
try { replay(form); } finally { replaying = false; }
|
||||
} finally {
|
||||
pending = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createNewPasswordSubmitHandler, isNewPasswordForm };
|
||||
@@ -115,6 +115,63 @@ AccountsTemplates.addFields([
|
||||
},
|
||||
]);
|
||||
|
||||
// PASSWORD_MIN_LENGTH and PASSWORD_CHECK_PWNED (models/lib/passwordPolicy.js).
|
||||
// useraccounts checks a NEW password's minLength in the browser - sign-up,
|
||||
// Change Password, reset, enrolment; never sign-in - and reads it from the
|
||||
// field at submit time, so the server's value applies once it has arrived.
|
||||
// Unless the minimum is enforced (PASSWORD_MIN_LENGTH_ENFORCE, off by
|
||||
// default) the fields keep their own 6.
|
||||
// The breached-password check runs before useraccounts sees the submit.
|
||||
if (Meteor.isClient) {
|
||||
const passwordPolicy = { minLength: 8, enforce: false, checkPwned: false };
|
||||
Meteor.call('getPasswordPolicy', (error, result) => {
|
||||
if (error || !result) return;
|
||||
Object.assign(passwordPolicy, result);
|
||||
if (!passwordPolicy.enforce) return;
|
||||
for (const id of ['password', 'password_again']) {
|
||||
const field = AccountsTemplates.getField(id);
|
||||
if (field) field.minLength = passwordPolicy.minLength;
|
||||
}
|
||||
});
|
||||
// Signing in is never refused for a short password: it is reported to
|
||||
// Admin Panel / Problems / Password length, and with the minimum enforced the
|
||||
// account is asked to change it (server/passwordChangeRequired.js). Only this
|
||||
// browser knows the length, so it is noted on the sign-in form - the length,
|
||||
// never the password - and compared with the minimum once the login worked.
|
||||
const { isNewPasswordForm } = require('/client/lib/newPasswordCheck');
|
||||
let signInPasswordLength = null;
|
||||
document.addEventListener('submit', event => {
|
||||
const form = event.target;
|
||||
const field = form && form.id === 'at-pwd-form' && form.querySelector('#at-field-password');
|
||||
if (field && !isNewPasswordForm(form)) signInPasswordLength = [...field.value].length;
|
||||
}, true);
|
||||
Accounts.onLogin(() => {
|
||||
const length = signInPasswordLength;
|
||||
signInPasswordLength = null;
|
||||
if (length === null) return;
|
||||
// Asked again: the minimum may have changed since the page loaded.
|
||||
Meteor.call('getPasswordPolicy', (error, result) => {
|
||||
if (error || !result || !(result.minLength > 0) || length >= result.minLength) return;
|
||||
Meteor.call('reportShortPasswordLogin', () => {
|
||||
const { checkPasswordChangeRequirement } = require('/client/components/users/passwordChangeRequired');
|
||||
checkPasswordChangeRequirement();
|
||||
});
|
||||
});
|
||||
});
|
||||
const { createNewPasswordSubmitHandler } = require('/client/lib/newPasswordCheck');
|
||||
document.addEventListener('submit', createNewPasswordSubmitHandler({
|
||||
policy: () => passwordPolicy,
|
||||
range: prefix => Meteor.callAsync('passwordPwnedRange', prefix),
|
||||
subtle: globalThis.crypto && globalThis.crypto.subtle,
|
||||
pwnedMessage: () => tr('password-pwned'),
|
||||
showError(message) {
|
||||
AccountsTemplates.setDisabled(false);
|
||||
AccountsTemplates.state.form.set('error', [message]);
|
||||
},
|
||||
replay: form => form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })),
|
||||
}), true);
|
||||
}
|
||||
|
||||
// The sign-in / register form field labels and placeholders were rendered through
|
||||
// accounts-t9n (`T9n.get`), whose per-language coverage is incomplete, and
|
||||
// useraccounts' `getPlaceholder()` never invokes a function placeholder (unlike
|
||||
@@ -333,6 +390,12 @@ AccountsTemplates.configure({
|
||||
forbidClientAccountCreation: disableRegistration,
|
||||
homeRoutePath: '/',
|
||||
onSubmitHook(error, state) {
|
||||
if (!error && state === 'changePwd') {
|
||||
// The server has signed every session of the account out, this one too
|
||||
// (server/passwordChangeRequired.js): sign in with the new password.
|
||||
Meteor.logout(() => FlowRouter.go('atSignIn'));
|
||||
return;
|
||||
}
|
||||
if (!error && (state === 'signUp' || state === 'signIn')) {
|
||||
hideTwoFactorPrompt();
|
||||
goHomeWhenSignedIn();
|
||||
|
||||
@@ -76,6 +76,8 @@ services:
|
||||
environment:
|
||||
DO_NOT_TRACK: "1"
|
||||
FERRETDB_TELEMETRY: disable
|
||||
# CVE-2026-98432: no MPTCP listener (Go's default since 1.24).
|
||||
GODEBUG: multipathtcp=0
|
||||
# Pin a specific release instead of the newest by setting e.g.
|
||||
# FERRETDB_RELEASE=download/v1.24.2
|
||||
FERRETDB_RELEASE: ${FERRETDB_RELEASE:-latest/download}
|
||||
@@ -906,6 +908,35 @@ services:
|
||||
# it too, and its value there wins.
|
||||
#- USERS_DISABLE_INACTIVE_AFTER_DAYS=180
|
||||
#---------------------------------------------------------------
|
||||
# LOGOUT_IDLE_MINUTES : Sign a browser session out after this many minutes without a key press, click,
|
||||
# touch or scroll. Empty or 0 = off (default). REST API tokens are not affected.
|
||||
#- LOGOUT_IDLE_MINUTES=30
|
||||
#---------------------------------------------------------------
|
||||
# REQUIRE_2FA : Compulsory two-factor authentication for accounts that sign in with a WeKan password:
|
||||
# off (default), admins or all. They must set it up right after signing in.
|
||||
#- REQUIRE_2FA=admins
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_MIN_LENGTH : Minimum password length, in characters. Default 8. A sign-in with a shorter
|
||||
# password is listed in Admin Panel / Problems / Password length; nothing is refused unless enforced:
|
||||
#- PASSWORD_MIN_LENGTH=12
|
||||
# PASSWORD_MIN_LENGTH_ENFORCE : true = refuse a shorter new password, and make an account that
|
||||
# signs in with a shorter one change it; the change signs every session out. Off by default.
|
||||
#- PASSWORD_MIN_LENGTH_ENFORCE=true
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_CHECK_PWNED : true = refuse a new password found in a known data breach. Off by default.
|
||||
# Only 5 hex digits of its SHA-1 are sent to api.pwnedpasswords.com.
|
||||
#- PASSWORD_CHECK_PWNED=true
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_MAX_AGE : Strict-Transport-Security max-age in seconds, sent only when ROOT_URL is https.
|
||||
# Default 15552000 (180 days). 0 = do not send it.
|
||||
#- HSTS_MAX_AGE=31536000
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_INCLUDE_SUBDOMAINS : true = HSTS covers every subdomain too. Only when ALL of them have HTTPS.
|
||||
#- HSTS_INCLUDE_SUBDOMAINS=false
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_PRELOAD : true = add the preload token (needs includeSubDomains and max-age >= 31536000).
|
||||
#- HSTS_PRELOAD=false
|
||||
#---------------------------------------------------------------
|
||||
# BACKUP_KEEP_DAYS : after each successful Admin Panel backup, delete
|
||||
# local (filesystem) backups older than this many days; the newest is
|
||||
# always kept. Unset or 0 = keep all (default). Admin Panel / Attachments
|
||||
|
||||
@@ -78,6 +78,8 @@ services:
|
||||
environment:
|
||||
DO_NOT_TRACK: "1"
|
||||
FERRETDB_TELEMETRY: disable
|
||||
# CVE-2026-98432: no MPTCP listener (Go's default since 1.24).
|
||||
GODEBUG: multipathtcp=0
|
||||
# Pin a specific release instead of the newest by setting e.g.
|
||||
# FERRETDB_RELEASE=download/v1.24.2
|
||||
FERRETDB_RELEASE: ${FERRETDB_RELEASE:-latest/download}
|
||||
@@ -908,6 +910,35 @@ services:
|
||||
# it too, and its value there wins.
|
||||
#- USERS_DISABLE_INACTIVE_AFTER_DAYS=180
|
||||
#---------------------------------------------------------------
|
||||
# LOGOUT_IDLE_MINUTES : Sign a browser session out after this many minutes without a key press, click,
|
||||
# touch or scroll. Empty or 0 = off (default). REST API tokens are not affected.
|
||||
#- LOGOUT_IDLE_MINUTES=30
|
||||
#---------------------------------------------------------------
|
||||
# REQUIRE_2FA : Compulsory two-factor authentication for accounts that sign in with a WeKan password:
|
||||
# off (default), admins or all. They must set it up right after signing in.
|
||||
#- REQUIRE_2FA=admins
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_MIN_LENGTH : Minimum password length, in characters. Default 8. A sign-in with a shorter
|
||||
# password is listed in Admin Panel / Problems / Password length; nothing is refused unless enforced:
|
||||
#- PASSWORD_MIN_LENGTH=12
|
||||
# PASSWORD_MIN_LENGTH_ENFORCE : true = refuse a shorter new password, and make an account that
|
||||
# signs in with a shorter one change it; the change signs every session out. Off by default.
|
||||
#- PASSWORD_MIN_LENGTH_ENFORCE=true
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_CHECK_PWNED : true = refuse a new password found in a known data breach. Off by default.
|
||||
# Only 5 hex digits of its SHA-1 are sent to api.pwnedpasswords.com.
|
||||
#- PASSWORD_CHECK_PWNED=true
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_MAX_AGE : Strict-Transport-Security max-age in seconds, sent only when ROOT_URL is https.
|
||||
# Default 15552000 (180 days). 0 = do not send it.
|
||||
#- HSTS_MAX_AGE=31536000
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_INCLUDE_SUBDOMAINS : true = HSTS covers every subdomain too. Only when ALL of them have HTTPS.
|
||||
#- HSTS_INCLUDE_SUBDOMAINS=false
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_PRELOAD : true = add the preload token (needs includeSubDomains and max-age >= 31536000).
|
||||
#- HSTS_PRELOAD=false
|
||||
#---------------------------------------------------------------
|
||||
# BACKUP_KEEP_DAYS : after each successful Admin Panel backup, delete
|
||||
# local (filesystem) backups older than this many days; the newest is
|
||||
# always kept. Unset or 0 = keep all (default). Admin Panel / Attachments
|
||||
|
||||
@@ -71,6 +71,8 @@ services:
|
||||
environment:
|
||||
DO_NOT_TRACK: "1"
|
||||
FERRETDB_TELEMETRY: disable
|
||||
# CVE-2026-98432: no MPTCP listener (Go's default since 1.24).
|
||||
GODEBUG: multipathtcp=0
|
||||
# Pin a specific release instead of the newest by setting e.g.
|
||||
# FERRETDB_RELEASE=download/v1.24.2
|
||||
FERRETDB_RELEASE: ${FERRETDB_RELEASE:-latest/download}
|
||||
@@ -895,6 +897,35 @@ services:
|
||||
# it too, and its value there wins.
|
||||
#- USERS_DISABLE_INACTIVE_AFTER_DAYS=180
|
||||
#---------------------------------------------------------------
|
||||
# LOGOUT_IDLE_MINUTES : Sign a browser session out after this many minutes without a key press, click,
|
||||
# touch or scroll. Empty or 0 = off (default). REST API tokens are not affected.
|
||||
#- LOGOUT_IDLE_MINUTES=30
|
||||
#---------------------------------------------------------------
|
||||
# REQUIRE_2FA : Compulsory two-factor authentication for accounts that sign in with a WeKan password:
|
||||
# off (default), admins or all. They must set it up right after signing in.
|
||||
#- REQUIRE_2FA=admins
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_MIN_LENGTH : Minimum password length, in characters. Default 8. A sign-in with a shorter
|
||||
# password is listed in Admin Panel / Problems / Password length; nothing is refused unless enforced:
|
||||
#- PASSWORD_MIN_LENGTH=12
|
||||
# PASSWORD_MIN_LENGTH_ENFORCE : true = refuse a shorter new password, and make an account that
|
||||
# signs in with a shorter one change it; the change signs every session out. Off by default.
|
||||
#- PASSWORD_MIN_LENGTH_ENFORCE=true
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_CHECK_PWNED : true = refuse a new password found in a known data breach. Off by default.
|
||||
# Only 5 hex digits of its SHA-1 are sent to api.pwnedpasswords.com.
|
||||
#- PASSWORD_CHECK_PWNED=true
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_MAX_AGE : Strict-Transport-Security max-age in seconds, sent only when ROOT_URL is https.
|
||||
# Default 15552000 (180 days). 0 = do not send it.
|
||||
#- HSTS_MAX_AGE=31536000
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_INCLUDE_SUBDOMAINS : true = HSTS covers every subdomain too. Only when ALL of them have HTTPS.
|
||||
#- HSTS_INCLUDE_SUBDOMAINS=false
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_PRELOAD : true = add the preload token (needs includeSubDomains and max-age >= 31536000).
|
||||
#- HSTS_PRELOAD=false
|
||||
#---------------------------------------------------------------
|
||||
# BACKUP_KEEP_DAYS : after each successful Admin Panel backup, delete
|
||||
# local (filesystem) backups older than this many days; the newest is
|
||||
# always kept. Unset or 0 = keep all (default). Admin Panel / Attachments
|
||||
|
||||
@@ -89,6 +89,8 @@ services:
|
||||
environment:
|
||||
DO_NOT_TRACK: "1"
|
||||
FERRETDB_TELEMETRY: disable
|
||||
# CVE-2026-98432: no MPTCP listener (Go's default since 1.24).
|
||||
GODEBUG: multipathtcp=0
|
||||
# Pin a specific release instead of the newest by setting e.g.
|
||||
# FERRETDB_RELEASE=download/v1.24.2
|
||||
FERRETDB_RELEASE: ${FERRETDB_RELEASE:-latest/download}
|
||||
@@ -912,6 +914,35 @@ services:
|
||||
# it too, and its value there wins.
|
||||
#- USERS_DISABLE_INACTIVE_AFTER_DAYS=180
|
||||
#---------------------------------------------------------------
|
||||
# LOGOUT_IDLE_MINUTES : Sign a browser session out after this many minutes without a key press, click,
|
||||
# touch or scroll. Empty or 0 = off (default). REST API tokens are not affected.
|
||||
#- LOGOUT_IDLE_MINUTES=30
|
||||
#---------------------------------------------------------------
|
||||
# REQUIRE_2FA : Compulsory two-factor authentication for accounts that sign in with a WeKan password:
|
||||
# off (default), admins or all. They must set it up right after signing in.
|
||||
#- REQUIRE_2FA=admins
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_MIN_LENGTH : Minimum password length, in characters. Default 8. A sign-in with a shorter
|
||||
# password is listed in Admin Panel / Problems / Password length; nothing is refused unless enforced:
|
||||
#- PASSWORD_MIN_LENGTH=12
|
||||
# PASSWORD_MIN_LENGTH_ENFORCE : true = refuse a shorter new password, and make an account that
|
||||
# signs in with a shorter one change it; the change signs every session out. Off by default.
|
||||
#- PASSWORD_MIN_LENGTH_ENFORCE=true
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_CHECK_PWNED : true = refuse a new password found in a known data breach. Off by default.
|
||||
# Only 5 hex digits of its SHA-1 are sent to api.pwnedpasswords.com.
|
||||
#- PASSWORD_CHECK_PWNED=true
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_MAX_AGE : Strict-Transport-Security max-age in seconds, sent only when ROOT_URL is https.
|
||||
# Default 15552000 (180 days). 0 = do not send it.
|
||||
#- HSTS_MAX_AGE=31536000
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_INCLUDE_SUBDOMAINS : true = HSTS covers every subdomain too. Only when ALL of them have HTTPS.
|
||||
#- HSTS_INCLUDE_SUBDOMAINS=false
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_PRELOAD : true = add the preload token (needs includeSubDomains and max-age >= 31536000).
|
||||
#- HSTS_PRELOAD=false
|
||||
#---------------------------------------------------------------
|
||||
# BACKUP_KEEP_DAYS : after each successful Admin Panel backup, delete
|
||||
# local (filesystem) backups older than this many days; the newest is
|
||||
# always kept. Unset or 0 = keep all (default). Admin Panel / Attachments
|
||||
|
||||
@@ -43,6 +43,8 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
FERRETDB_POSTGRESQL_URL: postgres://ferretdb:${FERRETDB_POSTGRES_PASSWORD:-ferretdb_secret}@postgres-documentdb/postgres
|
||||
# CVE-2026-98432: no MPTCP listener (Go's default since 1.24).
|
||||
GODEBUG: multipathtcp=0
|
||||
depends_on:
|
||||
- postgres-documentdb
|
||||
networks:
|
||||
@@ -818,6 +820,35 @@ services:
|
||||
# it too, and its value there wins.
|
||||
#- USERS_DISABLE_INACTIVE_AFTER_DAYS=180
|
||||
#---------------------------------------------------------------
|
||||
# LOGOUT_IDLE_MINUTES : Sign a browser session out after this many minutes without a key press, click,
|
||||
# touch or scroll. Empty or 0 = off (default). REST API tokens are not affected.
|
||||
#- LOGOUT_IDLE_MINUTES=30
|
||||
#---------------------------------------------------------------
|
||||
# REQUIRE_2FA : Compulsory two-factor authentication for accounts that sign in with a WeKan password:
|
||||
# off (default), admins or all. They must set it up right after signing in.
|
||||
#- REQUIRE_2FA=admins
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_MIN_LENGTH : Minimum password length, in characters. Default 8. A sign-in with a shorter
|
||||
# password is listed in Admin Panel / Problems / Password length; nothing is refused unless enforced:
|
||||
#- PASSWORD_MIN_LENGTH=12
|
||||
# PASSWORD_MIN_LENGTH_ENFORCE : true = refuse a shorter new password, and make an account that
|
||||
# signs in with a shorter one change it; the change signs every session out. Off by default.
|
||||
#- PASSWORD_MIN_LENGTH_ENFORCE=true
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_CHECK_PWNED : true = refuse a new password found in a known data breach. Off by default.
|
||||
# Only 5 hex digits of its SHA-1 are sent to api.pwnedpasswords.com.
|
||||
#- PASSWORD_CHECK_PWNED=true
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_MAX_AGE : Strict-Transport-Security max-age in seconds, sent only when ROOT_URL is https.
|
||||
# Default 15552000 (180 days). 0 = do not send it.
|
||||
#- HSTS_MAX_AGE=31536000
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_INCLUDE_SUBDOMAINS : true = HSTS covers every subdomain too. Only when ALL of them have HTTPS.
|
||||
#- HSTS_INCLUDE_SUBDOMAINS=false
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_PRELOAD : true = add the preload token (needs includeSubDomains and max-age >= 31536000).
|
||||
#- HSTS_PRELOAD=false
|
||||
#---------------------------------------------------------------
|
||||
# BACKUP_KEEP_DAYS : after each successful Admin Panel backup, delete
|
||||
# local (filesystem) backups older than this many days; the newest is
|
||||
# always kept. Unset or 0 = keep all (default). Admin Panel / Attachments
|
||||
|
||||
@@ -152,6 +152,8 @@ services:
|
||||
# environment:
|
||||
# # FerretDB stores everything in PostgreSQL
|
||||
# - FERRETDB_POSTGRESQL_URL=postgresql://wekan:wekan@wekan-postgres:5432/wekan
|
||||
# # CVE-2026-98432: no MPTCP listener (Go's default since 1.24)
|
||||
# - GODEBUG=multipathtcp=0
|
||||
# # Emulate MongoDB replica-set for Meteor OpLog (real-time updates)
|
||||
# - FERRETDB_REPLICATION_SET_NAME=rs0
|
||||
# # Suppress verbose debug output
|
||||
@@ -1049,6 +1051,35 @@ services:
|
||||
# it too, and its value there wins.
|
||||
#- USERS_DISABLE_INACTIVE_AFTER_DAYS=180
|
||||
#---------------------------------------------------------------
|
||||
# LOGOUT_IDLE_MINUTES : Sign a browser session out after this many minutes without a key press, click,
|
||||
# touch or scroll. Empty or 0 = off (default). REST API tokens are not affected.
|
||||
#- LOGOUT_IDLE_MINUTES=30
|
||||
#---------------------------------------------------------------
|
||||
# REQUIRE_2FA : Compulsory two-factor authentication for accounts that sign in with a WeKan password:
|
||||
# off (default), admins or all. They must set it up right after signing in.
|
||||
#- REQUIRE_2FA=admins
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_MIN_LENGTH : Minimum password length, in characters. Default 8. A sign-in with a shorter
|
||||
# password is listed in Admin Panel / Problems / Password length; nothing is refused unless enforced:
|
||||
#- PASSWORD_MIN_LENGTH=12
|
||||
# PASSWORD_MIN_LENGTH_ENFORCE : true = refuse a shorter new password, and make an account that
|
||||
# signs in with a shorter one change it; the change signs every session out. Off by default.
|
||||
#- PASSWORD_MIN_LENGTH_ENFORCE=true
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_CHECK_PWNED : true = refuse a new password found in a known data breach. Off by default.
|
||||
# Only 5 hex digits of its SHA-1 are sent to api.pwnedpasswords.com.
|
||||
#- PASSWORD_CHECK_PWNED=true
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_MAX_AGE : Strict-Transport-Security max-age in seconds, sent only when ROOT_URL is https.
|
||||
# Default 15552000 (180 days). 0 = do not send it.
|
||||
#- HSTS_MAX_AGE=31536000
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_INCLUDE_SUBDOMAINS : true = HSTS covers every subdomain too. Only when ALL of them have HTTPS.
|
||||
#- HSTS_INCLUDE_SUBDOMAINS=false
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_PRELOAD : true = add the preload token (needs includeSubDomains and max-age >= 31536000).
|
||||
#- HSTS_PRELOAD=false
|
||||
#---------------------------------------------------------------
|
||||
# BACKUP_KEEP_DAYS : after each successful Admin Panel backup, delete
|
||||
# local (filesystem) backups older than this many days; the newest is
|
||||
# always kept. Unset or 0 = keep all (default). Admin Panel / Attachments
|
||||
|
||||
@@ -52,6 +52,8 @@ services:
|
||||
environment:
|
||||
DO_NOT_TRACK: "1"
|
||||
FERRETDB_TELEMETRY: disable
|
||||
# CVE-2026-98432: no MPTCP listener (Go's default since 1.24).
|
||||
GODEBUG: multipathtcp=0
|
||||
# Follow the newest published release. A specific download/vX.Y.Z remains
|
||||
# available for rollback; the marker below upgrades an older cached binary.
|
||||
FERRETDB_RELEASE: ${FERRETDB_RELEASE:-latest/download}
|
||||
@@ -893,6 +895,35 @@ services:
|
||||
# it too, and its value there wins.
|
||||
#- USERS_DISABLE_INACTIVE_AFTER_DAYS=180
|
||||
#---------------------------------------------------------------
|
||||
# LOGOUT_IDLE_MINUTES : Sign a browser session out after this many minutes without a key press, click,
|
||||
# touch or scroll. Empty or 0 = off (default). REST API tokens are not affected.
|
||||
#- LOGOUT_IDLE_MINUTES=30
|
||||
#---------------------------------------------------------------
|
||||
# REQUIRE_2FA : Compulsory two-factor authentication for accounts that sign in with a WeKan password:
|
||||
# off (default), admins or all. They must set it up right after signing in.
|
||||
#- REQUIRE_2FA=admins
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_MIN_LENGTH : Minimum password length, in characters. Default 8. A sign-in with a shorter
|
||||
# password is listed in Admin Panel / Problems / Password length; nothing is refused unless enforced:
|
||||
#- PASSWORD_MIN_LENGTH=12
|
||||
# PASSWORD_MIN_LENGTH_ENFORCE : true = refuse a shorter new password, and make an account that
|
||||
# signs in with a shorter one change it; the change signs every session out. Off by default.
|
||||
#- PASSWORD_MIN_LENGTH_ENFORCE=true
|
||||
#---------------------------------------------------------------
|
||||
# PASSWORD_CHECK_PWNED : true = refuse a new password found in a known data breach. Off by default.
|
||||
# Only 5 hex digits of its SHA-1 are sent to api.pwnedpasswords.com.
|
||||
#- PASSWORD_CHECK_PWNED=true
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_MAX_AGE : Strict-Transport-Security max-age in seconds, sent only when ROOT_URL is https.
|
||||
# Default 15552000 (180 days). 0 = do not send it.
|
||||
#- HSTS_MAX_AGE=31536000
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_INCLUDE_SUBDOMAINS : true = HSTS covers every subdomain too. Only when ALL of them have HTTPS.
|
||||
#- HSTS_INCLUDE_SUBDOMAINS=false
|
||||
#---------------------------------------------------------------
|
||||
# HSTS_PRELOAD : true = add the preload token (needs includeSubDomains and max-age >= 31536000).
|
||||
#- HSTS_PRELOAD=false
|
||||
#---------------------------------------------------------------
|
||||
# BACKUP_KEEP_DAYS : after each successful Admin Panel backup, delete
|
||||
# local (filesystem) backups older than this many days; the newest is
|
||||
# always kept. Unset or 0 = keep all (default). Admin Panel / Attachments
|
||||
|
||||
+30
-18
@@ -272,7 +272,7 @@ examples do not back up a FerretDB SQLite directory or external file storage.
|
||||
|
||||
# Backup Sandstorm
|
||||
|
||||
1. Please first backup your Sandstorm https://docs.sandstorm.io/en/latest/administering/backups/
|
||||
1. Please first backup your Sandstorm https://docs.sandstorm.org/en/latest/administering/backups/
|
||||
2. Please first download all of your grains to .zip files from top menu row down arrow button, just in case.
|
||||
|
||||
# Upgrade Sandstorm WeKan
|
||||
@@ -329,15 +329,20 @@ docker compose up -d
|
||||
# Backup Wekan Snap to directory dump
|
||||
```bash
|
||||
sudo snap stop wekan.wekan
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan/current/lib/x86_64-linux-gnu
|
||||
export PATH="/snap/wekan/current/bin:$PATH"
|
||||
S=/snap; [ -d /snap/wekan ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan/current/lib/$(uname -m)-linux-gnu"
|
||||
export PATH="$S/wekan/current/bin:$PATH"
|
||||
mongodump --port 27019
|
||||
sudo snap get wekan > snap-settings.sh
|
||||
```
|
||||
NOTE for Arch Linux: Use this path instead. [Source](https://github.com/wekan/wekan/issues/3941).
|
||||
```bash
|
||||
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH-}:/var/lib/snapd/snap/wekan/current/lib/x86_64-linux-gnu
|
||||
```
|
||||
The `S=` line picks where snapd keeps the snaps on this host. Ubuntu and Debian
|
||||
have `/snap`; Fedora, RHEL, AlmaLinux, Rocky Linux, CentOS Stream and Arch Linux
|
||||
keep them in `/var/lib/snapd/snap` and have no `/snap` unless somebody has made
|
||||
the symlink (`sudo ln -s /var/lib/snapd/snap /snap`). The data is in
|
||||
`/var/snap/wekan/common` on all of them. `$(uname -m)` picks the library
|
||||
directory of the CPU: `x86_64-linux-gnu` on amd64, `aarch64-linux-gnu` on arm64.
|
||||
[Arch Linux report](https://github.com/wekan/wekan/issues/3941).
|
||||
|
||||
username is your /home/username
|
||||
```bash
|
||||
sudo chown username:username snap-settings.sh
|
||||
@@ -355,8 +360,9 @@ chmod +x snap-settings.sh
|
||||
# Restore Wekan Snap
|
||||
```bash
|
||||
sudo snap stop wekan.wekan
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan/current/lib/x86_64-linux-gnu
|
||||
export PATH="$PATH:/snap/wekan/current/bin"
|
||||
S=/snap; [ -d /snap/wekan ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan/current/lib/$(uname -m)-linux-gnu"
|
||||
export PATH="$PATH:$S/wekan/current/bin"
|
||||
mongorestore --drop --port 27019
|
||||
sudo snap start wekan.wekan
|
||||
./snap-settings.sh
|
||||
@@ -454,8 +460,9 @@ sudo snap refresh
|
||||
# Backup Wekan Gantt GPLv2 Snap to directory dump
|
||||
```bash
|
||||
sudo snap stop wekan-gantt-gpl.wekan
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan-gantt-gpl/current/lib/x86_64-linux-gnu
|
||||
export PATH="$PATH:/snap/wekan-gantt-gpl/current/bin"
|
||||
S=/snap; [ -d /snap/wekan-gantt-gpl ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan-gantt-gpl/current/lib/$(uname -m)-linux-gnu"
|
||||
export PATH="$PATH:$S/wekan-gantt-gpl/current/bin"
|
||||
mongodump --port 27019
|
||||
sudo snap get wekan-gantt-gpl > snap-settings.sh
|
||||
```
|
||||
@@ -476,8 +483,9 @@ chmod +x snap-settings.sh
|
||||
# Restore Wekan Gantt GPLv2 Snap
|
||||
```bash
|
||||
sudo snap stop wekan-gantt-gpl.wekan
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan-gantt-gpl/current/lib/x86_64-linux-gnu
|
||||
export PATH="$PATH:/snap/wekan-gantt-gpl/current/bin"
|
||||
S=/snap; [ -d /snap/wekan-gantt-gpl ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan-gantt-gpl/current/lib/$(uname -m)-linux-gnu"
|
||||
export PATH="$PATH:$S/wekan-gantt-gpl/current/bin"
|
||||
mongorestore --drop --port 27019
|
||||
sudo snap start wekan-gantt-gpl.wekan
|
||||
./snap-settings.sh
|
||||
@@ -820,8 +828,9 @@ You could use [daff](https://github.com/paulfitz/daff) to compare tables.
|
||||
|
||||
Add to your `~/.bashrc`
|
||||
```bash
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan/current/lib/x86_64-linux-gnu
|
||||
export PATH="$PATH:/snap/wekan/current/bin"
|
||||
S=/snap; [ -d /snap/wekan ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan/current/lib/$(uname -m)-linux-gnu"
|
||||
export PATH="$PATH:$S/wekan/current/bin"
|
||||
```
|
||||
Then you can backup:
|
||||
```bash
|
||||
@@ -838,7 +847,8 @@ mongoshell.sh
|
||||
```bash
|
||||
#/bin/bash
|
||||
export LC_ALL=C
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan/current/lib/x86_64-linux-gnu
|
||||
S=/snap; [ -d /snap/wekan ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan/current/lib/$(uname -m)-linux-gnu"
|
||||
version=$(snap list | grep wekan | awk -F ' ' '{print $3}')
|
||||
mongo=$"/snap/wekan/$version/bin/mongo"
|
||||
$mongo --port 27019
|
||||
@@ -856,7 +866,8 @@ wekan-backup.sh
|
||||
```bash
|
||||
#!/bin/bash
|
||||
export LC_ALL=C
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan/current/lib/x86_64-linux-gnu
|
||||
S=/snap; [ -d /snap/wekan ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan/current/lib/$(uname -m)-linux-gnu"
|
||||
|
||||
version=$(snap list | grep wekan | awk -F ' ' '{print $3}')
|
||||
now=$(date +"%Y%m%d-%H%M%S")
|
||||
@@ -907,7 +918,8 @@ wekan-restore.sh
|
||||
```bash
|
||||
#!/bin/bash
|
||||
export LC_ALL=C
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan/current/lib/x86_64-linux-gnu
|
||||
S=/snap; [ -d /snap/wekan ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan/current/lib/$(uname -m)-linux-gnu"
|
||||
|
||||
makesRestore()
|
||||
{
|
||||
|
||||
@@ -5,9 +5,10 @@ sudo su
|
||||
|
||||
snap stop wekan
|
||||
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan/current/lib/x86_64-linux-gnu
|
||||
S=/snap; [ -d /snap/wekan ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan/current/lib/$(uname -m)-linux-gnu"
|
||||
|
||||
export PATH="/snap/wekan/current/bin:$PATH"
|
||||
export PATH="$S/wekan/current/bin:$PATH"
|
||||
|
||||
mongod --dbpath "/var/snap/wekan/common" --repair >> /root/repairlog.txt
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ export PATH="/snap/wekan/current/migratemongo/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/snap/wekan/current/migratemongo/lib:$LD_LIBRARY_PATH"
|
||||
|
||||
# MongoDB 7.x (default)
|
||||
export PATH="/snap/wekan/current/bin:$PATH"
|
||||
S=/snap; [ -d /snap/wekan ] || S=/var/lib/snapd/snap
|
||||
export PATH="$S/wekan/current/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/snap/wekan/current/usr/lib:$LD_LIBRARY_PATH"
|
||||
```
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ or have Javascript disabled for security reasons.
|
||||
- Pascal code with TRSE https://lemonspawn.com/turbo-rascal-syntax-error-expected-but-begin/
|
||||
- Embedded webserver, for example:
|
||||
- C https://github.com/wekan/hx/blob/main/prototypes/code/webserver/works/c/server.c
|
||||
- Sandstorm https://sandstorm.io , https://github.com/sandstorm-io/sandstorm
|
||||
- Sandstorm https://sandstorm.org , https://github.com/sandstorm-io/sandstorm
|
||||
- Rust https://github.com/dwrensha/sandstorm-rawapi-example-rust
|
||||
- C++ https://github.com/dwrensha/sandstorm-rawapi-example
|
||||
- Try to avoid strange NPM packages
|
||||
|
||||
@@ -257,7 +257,9 @@ plugin npm dependencies. `METEOR_PROFILE` defaults to 100 milliseconds, exposing
|
||||
Meteor tool timing diagnostics when available. Profiling output may appear only
|
||||
when a measured operation finishes; elapsed status still appears while it is quiet.
|
||||
|
||||
During builds, Node module debugging prints module resolution as it happens. The
|
||||
Node module debugging, which prints every module resolution as it happens, is
|
||||
off unless `WEKAN_BUILD_TRACE_MODULES=1` is set: a Meteor build writes that trace
|
||||
at gigabytes a minute (1.1 GB in the first three minutes of a dev build). The
|
||||
local `tools/build-command-output.cjs` preload prints resolver subprocess commands
|
||||
and echoes their captured stdout and stderr while preserving exit codes and callback
|
||||
results. Both terminal and build log receive this output. Command arguments redact
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Adding a "# Upcoming WeKan ® release" section to CHANGELOG.md
|
||||
# Adding a "# Upcoming WeKan ® release" section to CHANGELOG-big.md
|
||||
|
||||
`CHANGELOG.md` does **not** carry an empty `# Upcoming WeKan ® release`
|
||||
`CHANGELOG-big.md` does **not** carry an empty `# Upcoming WeKan ® release`
|
||||
placeholder between releases. `releases/release-all.sh` used to auto-create one
|
||||
immediately after every release, with an `**In short:** nothing here yet.`
|
||||
paragraph — so the file always had a section that said nothing, sitting there
|
||||
|
||||
@@ -96,7 +96,7 @@ Wekan:
|
||||
|
||||
Wekan for Sandstorm:
|
||||
- Install above Wekan from source
|
||||
- Install [Sandstorm locally](https://sandstorm.io/install) with `curl https://install.sandstorm.io | bash`, select dev install
|
||||
- Install [Sandstorm locally](https://sandstorm.org/install) with `curl https://install.sandstorm.org | bash`, select dev install
|
||||
- Install [meteor-spk](https://github.com/sandstorm-io/meteor-spk)
|
||||
- Get 100% CPU issue fibers fixed node, and copy it to spk directory:<br />
|
||||
`wget https://releases.wekan.team/node`<br />
|
||||
@@ -105,7 +105,7 @@ Wekan for Sandstorm:
|
||||
- Add to your /home/username/.bashrc : <br /> `export PATH=$PATH:$HOME/projects/meteor-spk/meteor-spk-0.4.0`
|
||||
- Close and open your terminal, or read settings from .bashrc with<br />`source ~/.bashrc`
|
||||
- `cd wekan && meteor-spk dev`
|
||||
- Then Wekan will be visible at local sandstorm at http://local.sandstorm.io:6080/
|
||||
- Then Wekan will be visible at local sandstorm at http://local.sandstorm.org:6080/
|
||||
- Sandstorm commands: `sudo sandstorm`. [Release scripts](https://github.com/wekan/wekan-maintainer/tree/master/releases). Official releases require publishing key that only xet7 has.
|
||||
|
||||
Docker:
|
||||
|
||||
@@ -22,7 +22,7 @@ for years this page described four directories out of twenty and did not say so.
|
||||
| [sandstorm-src/](../../sandstorm-src) | the Sandstorm package |
|
||||
| [openapi/](../../openapi) | the REST API description, generated from the routes |
|
||||
| [meta/](../../meta) | signatures, icons, screenshots, project description |
|
||||
| [old-CHANGELOG/](../../old-CHANGELOG) | the CHANGELOG's history, by year and by month, a busy month in parts (`CHANGELOG.md` holds the newest releases that fit GitHub's 500 KiB Markdown limit) |
|
||||
| [old-CHANGELOG/](../../old-CHANGELOG) | the CHANGELOG's history, by year and by month, a busy month in parts (`CHANGELOG-big.md` holds the Status, the upcoming release and the newest releases that fit GitHub's 500 KiB Markdown limit; `CHANGELOG.md` only the In short of the upcoming release and the five newest) |
|
||||
| `.tools/` | NOT part of this repository: the companion repos and toolchains a build needs, ignored by git and by Meteor |
|
||||
| `node_modules/`, `.meteor/`, `.build/`, `_build/` | generated; never edited, never committed |
|
||||
|
||||
@@ -234,7 +234,7 @@ Other files:
|
||||
* **[snap](../../snap)**;
|
||||
* **[snap-src](../../snap-src)**;
|
||||
* Sandstorm:
|
||||
* **[sandstorm.js](../../sandstorm.js)**: [Sandstorm](https://sandstorm.io/) specific code;
|
||||
* **[sandstorm.js](../../sandstorm.js)**: [Sandstorm](https://sandstorm.org/) specific code;
|
||||
* **[sandstorm-pkgdef.capnp](../../sandstorm-pkgdef.capnp)**: used the meteor-spk tool to generate a sandstorm package;
|
||||
* Node:
|
||||
* **[package.json](../../package.json)**;
|
||||
@@ -273,7 +273,10 @@ Other files:
|
||||
* **[translations/](../../releases/translations)** - the Transifex pull and the
|
||||
per-key merge that never overwrites a human translation.
|
||||
* **[changelog-archive.mjs](../../releases/changelog-archive.mjs)** - moves
|
||||
finished months out of `CHANGELOG.md` into `old-CHANGELOG/`.
|
||||
finished months out of `CHANGELOG-big.md` into `old-CHANGELOG/`.
|
||||
* **[changelog-short.mjs](../../releases/changelog-short.mjs)** - writes
|
||||
`CHANGELOG.md`, the In short of the upcoming release and the five newest,
|
||||
from `CHANGELOG-big.md`.
|
||||
* **[tests/](../../tests)** - `*.test.cjs` suites run by
|
||||
**[run-node-suites.cjs](../../tests/run-node-suites.cjs)** (all of them, with
|
||||
every failure listed at the end), plus the Playwright browser tests and the
|
||||
@@ -286,7 +289,8 @@ Other files:
|
||||
# Info
|
||||
|
||||
* **[meta](../../meta)**: binary signatures, project description, icons, screenshots and, oui, a French change-log;
|
||||
* **[CHANGELOG.md](../../CHANGELOG.md)**;
|
||||
* **[CHANGELOG.md](../../CHANGELOG.md)** - the upcoming release and the five newest, in short, and
|
||||
**[CHANGELOG-big.md](../../CHANGELOG-big.md)** - every change, the upcoming release and the status;
|
||||
* **[Contributing.md](../../CONTRIBUTING.md)**;
|
||||
* **[LICENSE](../../LICENSE)**;
|
||||
* **[README.md](../../README.md)**.
|
||||
|
||||
@@ -134,11 +134,11 @@ docker-compose up -d --build
|
||||
|
||||
## Sandstorm
|
||||
|
||||
1) Install local development version of Sandstorm to your laptop from https://sandstorm.io/install
|
||||
1) Install local development version of Sandstorm to your laptop from https://sandstorm.org/install
|
||||
|
||||
2) Download and install experimental version to your local Sandstorm:
|
||||
[Sandstorm](../Platforms/FOSS/Container/Sandstorm)
|
||||
Local sandstorm is at http://local.sandstorm.io:6080/ .
|
||||
Local sandstorm is at http://local.sandstorm.org:6080/ .
|
||||
|
||||
3) Download your production Wekan grain .zip file with down arrow button. Upload to your dev local Sandstorm.
|
||||
Try does it work.
|
||||
+4
-4
@@ -40,7 +40,7 @@ It's a very specific niche, with limited amount competitors, with all of this ap
|
||||
|
||||
## Requirements
|
||||
|
||||
- 64bit: Linux [Snap](https://github.com/wekan/wekan-snap/wiki/Install) or [Sandstorm](https://sandstorm.io) /
|
||||
- 64bit: Linux [Snap](https://github.com/wekan/wekan-snap/wiki/Install) or [Sandstorm](https://sandstorm.org) /
|
||||
[Mac](../Platforms/Propietary/OS/Mac) / [Windows](../Platforms/Propietary/OS/Windows/Install-Wekan-from-source-on-Windows.md).
|
||||
[More Platforms](../Platforms). [ARM progress](https://github.com/wekan/wekan/issues/1053#issuecomment-410919264).
|
||||
- 1 GB RAM minimum free for Wekan. Production server should have miminum total 4 GB RAM.
|
||||
@@ -105,7 +105,7 @@ Wekan:
|
||||
- Snap/Docker/Sandstorm versions runs inside sandbox and does not have any access to elsewhere on server filesystem.
|
||||
- Snap version of Wekan is built directly from https://github.com/wekan/wekan repo on Canonical's build service that does security checks etc.
|
||||
- Docker version of Wekan is built directly from https://github.com/wekan/wekan by [Quay](https://quay.io/wekan/wekan) and [Docker Hub](https://hub.docker.com/r/wekanteam/wekan/) and they do security checks etc.
|
||||
- Sandstorm version is checked by [ocdtrekkie](https://github.com/ocdtrekkie) that it does not break anything, before he lets new version of Wekan into [Sandstorm App Market](https://apps.sandstorm.io/app/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h).
|
||||
- Sandstorm version is checked by [ocdtrekkie](https://github.com/ocdtrekkie) that it does not break anything, before he lets new version of Wekan into [Sandstorm App Market](https://apps.sandstorm.org/app/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h).
|
||||
- Wekan does not load any files from Internet, when starting Wekan. For further restrictions, you could on internal server only allow incoming and outgoing traffic from
|
||||
on internal server firewall to those computers that need to use Wekan.
|
||||
- There is contributors to Wekan from all over the world, that add fixes and features to Wekan.
|
||||
@@ -242,8 +242,8 @@ Add only one link to this FAQ. Do not in any way comment or feed the trolls.
|
||||
Not a Company, Not a Startup, Not a Product with Enterprise version. Everything is now [Open Source](https://en.wikipedia.org/wiki/Open-source_software) and [Free software](https://en.wikipedia.org/wiki/Free_software).
|
||||
|
||||
## What is Sandstorm?
|
||||
[Sandstorm](https://sandstorm.io) is a open-source and free software security audited platform with grains, logging, admin settings, server clustering and App Market. App Market has Wekan as installable App. SSO options like LDAP, passwordless email, SAML, GitHub and Google Auth are already available on Sandstorm. Sandstorm is preferred platform for Wekan, as it would take a lot of work to reimplement everything in standalone Wekan.
|
||||
[Sandstorm](https://sandstorm.org) is a open-source and free software security audited platform with grains, logging, admin settings, server clustering and App Market. App Market has Wekan as installable App. SSO options like LDAP, passwordless email, SAML, GitHub and Google Auth are already available on Sandstorm. Sandstorm is preferred platform for Wekan, as it would take a lot of work to reimplement everything in standalone Wekan.
|
||||
|
||||
## How can you contribute to Sandstorm?
|
||||
See [Sandstorm website about contributing pull requests](https://sandstorm.io) and [returning to Open Source community roots, including donation info](https://sandstorm.io/news/2017-02-06-sandstorm-returning-to-community-roots).
|
||||
See [Sandstorm website about contributing pull requests](https://sandstorm.org) and [returning to Open Source community roots, including donation info](https://sandstorm.org/news/2017-02-06-sandstorm-returning-to-community-roots).
|
||||
|
||||
+4
-4
@@ -63,10 +63,10 @@ GitHub Username | Chat Username | Location | Position | Details
|
||||
[Zokormazo](https://github.com/Zokormazo) | Zokormazo | Spain | Contributor | [Add isAdmin to user Schema](https://github.com/wekan/wekan/pull/970), [Add TLS toggle to Admin Panel SMTP setting](https://github.com/wekan/wekan/pull/960)
|
||||
[shtefcs](https://github.com/shtefcs) | kinder | Universe | [Vanila](https://vanila.io) Founder | [Chat](https://chat.vanila.io/channel/wekan), growth hacking
|
||||
[jelenajjo](https://github.com/jelenajjo) | jelena | Universe | [Vanila](https://vanila.io) Web Developer | [Themes](https://github.com/wekan/wekan/issues/781), upcoming new website
|
||||
[dwrensha](https://github.com/dwrensha) | - | USA | [Sandstorm](https://sandstorm.io) Developer | Packaging Wekan releases for Sandstorm
|
||||
[kentonv](https://github.com/kentonv) | - | USA | [Sandstorm](https://sandstorm.io) Developer | [Cap’n Proto](https://capnproto.org) and [node-capnp](https://github.com/kentonv/node-capnp)
|
||||
[ocdtrekkie](https://github.com/ocdtrekkie) | ocdtrekkie | USA | [Sandstorm](https://sandstorm.io) user from beginning of Sandstorm | Testing and accepting Wekan releases for Sandstorm
|
||||
[JamborJan](https://github.com/JamborJan) | - | Universe | [Sandstorm](https://sandstorm.io) Developer | [Add bigger screenshots](https://github.com/wekan/wekan/pull/940)
|
||||
[dwrensha](https://github.com/dwrensha) | - | USA | [Sandstorm](https://sandstorm.org) Developer | Packaging Wekan releases for Sandstorm
|
||||
[kentonv](https://github.com/kentonv) | - | USA | [Sandstorm](https://sandstorm.org) Developer | [Cap’n Proto](https://capnproto.org) and [node-capnp](https://github.com/kentonv/node-capnp)
|
||||
[ocdtrekkie](https://github.com/ocdtrekkie) | ocdtrekkie | USA | [Sandstorm](https://sandstorm.org) user from beginning of Sandstorm | Testing and accepting Wekan releases for Sandstorm
|
||||
[JamborJan](https://github.com/JamborJan) | - | Universe | [Sandstorm](https://sandstorm.org) Developer | [Add bigger screenshots](https://github.com/wekan/wekan/pull/940)
|
||||
[nebulade](https://github.com/nebulade) | - | Germany | [Cloudron](https://cloudron.io) Developer | [Re-enable export](https://github.com/wekan/wekan/pull/1059)
|
||||
[kubiko](https://github.com/kubiko) | - | UK | [Canonical](https://www.canonical.com) Developer | [snap package](https://github.com/wekan/wekan-snap), [Adding snapcraft build support from source](https://github.com/wekan/wekan/pull/1091), [Use version scriptlet in snap](https://github.com/wekan/wekan/pull/1164)
|
||||
[whowillcare](https://github.com/whowillcare) | Universe | Contributor | Global webhook to Admin Panel, etc a lot of features.
|
||||
|
||||
@@ -58,6 +58,7 @@ OAuth providers and Passwordless login each have their own menu entry.
|
||||
| `/admin/people/organizations` | Organizations | `org-setting` |
|
||||
| `/admin/people/teams` | Teams | `team-setting` |
|
||||
| `/admin/people/locked-users` | Locked Users | `locked-users-setting` |
|
||||
| `/admin/people/audit-log` | Audit log | `audit-log-setting` |
|
||||
| `/admin/people/roles` | Roles | `roles-setting` |
|
||||
| `/admin/people/shared-templates` | Shared templates | `templates-setting` |
|
||||
| `/admin/people/login` | Login | `registration-setting` |
|
||||
@@ -80,6 +81,7 @@ OAuth providers and Passwordless login each have their own menu entry.
|
||||
| `/admin/problems/backup` | Backup (status and history; settings stay in Attachments / Backup) | `features-backup` |
|
||||
| `/admin/problems/security-report` | Security Report | `report-security` |
|
||||
| `/admin/problems/impersonation` | Impersonation Report | `report-impersonation` |
|
||||
| `/admin/problems/password-length` | Password length | `report-password-length` |
|
||||
| `/admin/problems/performance` | Performance | `features-performance` |
|
||||
| `/admin/problems/speed` | Speed | `report-speed` |
|
||||
| `/admin/problems/tests` | Tests | `report-tests` |
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# People / Audit log
|
||||
|
||||
`/admin/people/audit-log`, site administrators only.
|
||||
|
||||
Every administrative action of a site administrator, newest first, one row
|
||||
each: when, who, which action, whether it succeeded (a refused call is shown
|
||||
too, with its error), its arguments and the address it came from. The search
|
||||
box matches the administrator, the action and the arguments.
|
||||
|
||||
What is recorded: changes to people (Add user, Set password, username, email,
|
||||
admin and active flags, removing a user, impersonation, unlocking), to
|
||||
organizations and teams, to login and site settings, to attachment storage,
|
||||
and backups, restores and data repairs. An administrator editing cards is a
|
||||
member editing cards and is not recorded. The list of actions is
|
||||
`AUDITED_METHODS` in `models/lib/adminAudit.js`.
|
||||
|
||||
A password or a secret is never recorded: values under a credential-named key,
|
||||
the arguments that are a password by position and a password inside a URL are
|
||||
shown as `[redacted]`.
|
||||
|
||||
Entries are kept for a year and then removed. Attacks WeKan refused are in
|
||||
Admin Panel / Problems instead; see [Security settings](../../../Security/Security-Settings.md).
|
||||
@@ -19,6 +19,7 @@ Who may sign in, who they belong to, and what they may do — in menu order.
|
||||
| People / Teams | `teams` | [Teams.md](Teams.md) | Teams, their per-team switches, and the same-Team board restriction. |
|
||||
| People / People | `people` | [People.md](People.md) | Every user: e-mail, admin flag, active state, lockout, created date. |
|
||||
| People / Locked Users | `locked-users` | [Locked-Users.md](Locked-Users.md) | Brute-force protection settings and who is locked out. |
|
||||
| People / Audit log | `audit-log` | [Audit-Log.md](Audit-Log.md) | What site administrators did, newest first; kept for a year. |
|
||||
| People / Roles | `roles` | [Roles.md](Roles.md) | Which board roles may invite people to a board. |
|
||||
| People / Shared templates | `shared-templates` | [Shared-Templates.md](Shared-Templates.md) | Which scopes may share template boards. |
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Admin Panel / Problems / Password length
|
||||
|
||||
Who signs in with a password shorter than the minimum length, from where, and
|
||||
whether they have changed it since. The URL is `/admin/problems/password-length`.
|
||||
|
||||
## Settings
|
||||
|
||||
Above the table, with one **Save** button:
|
||||
|
||||
| Setting | Environment variable | Default | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| Minimum password length | `PASSWORD_MIN_LENGTH` | `8` | A sign-in with a shorter password is listed in the table below. Nothing is refused for it. |
|
||||
| Enforce the minimum length | `PASSWORD_MIN_LENGTH_ENFORCE` | off | A shorter **new** password is refused (sign-up, Change Password, reset, enrolment, an administrator's Set password and Add user, `POST /users/register`), and an account that signs in with an older shorter password sees only **Change your password** until it has a long enough one. |
|
||||
|
||||
Both are also in **Admin Panel / People / Login**, and saving either page saves
|
||||
the same setting. A value saved here overrides the environment variable; the
|
||||
page says so when an environment variable is set.
|
||||
|
||||
**Signing in is never refused** for a short password, enforced or not: nobody
|
||||
is locked out by a minimum set after their password was. Changing a password -
|
||||
forced or not, in Member Settings or with a reset link - signs **every** session
|
||||
of the account out, the one that changed it too, and the account then signs in
|
||||
with the new password.
|
||||
|
||||
LDAP, OAuth2, OIDC, SAML, CAS and Sandstorm accounts are neither listed nor
|
||||
asked: their password is their provider's, and so is its policy.
|
||||
|
||||
## The table
|
||||
|
||||
One row per account, never one per sign-in, like every Problems report: an
|
||||
account that signs in every day is one row whose count grows.
|
||||
|
||||
| Column | What it shows |
|
||||
| --- | --- |
|
||||
| Username | The account. |
|
||||
| First login | The first time it signed in with a password shorter than the minimum. |
|
||||
| IPv4 address | The latest IPv4 address it did so from, with the country's flag and the city when a CDN or proxy in front of WeKan said where the address is. |
|
||||
| IPv6 address | The same for IPv6. |
|
||||
| Last login | The latest time it signed in with a short password. |
|
||||
| Times logged in | How many times it has. |
|
||||
| Fixed | When its password was changed. Empty while it is still too short. |
|
||||
|
||||
The filter above the table, as on every [table page](../../General/Table.md):
|
||||
|
||||
| Filter | Shows |
|
||||
| --- | --- |
|
||||
| **Too short password** (default) | Accounts that still have a short password. |
|
||||
| **Fixed to long enough password** | Accounts that changed theirs. |
|
||||
| **All** | Both. |
|
||||
|
||||
The search box matches the username and the addresses.
|
||||
|
||||
## How WeKan knows a password is short
|
||||
|
||||
Meteor sends a password to the server hashed, so the server never sees how long
|
||||
it is. The browser that typed it does: after a sign-in that worked, it compares
|
||||
the length with the minimum and, when it is shorter, tells the server "this one
|
||||
was short" - never the password and never its length.
|
||||
|
||||
The same is true of a password changed through a form, so **Fixed** means the
|
||||
password was changed and the account has not signed in with a short one since.
|
||||
An account that changes its password to another short one is listed as too short
|
||||
again the next time it signs in. An administrator's Set password is the one
|
||||
change the server can measure, so a short one set there does not count as fixed.
|
||||
|
||||
Where an address is comes only from headers a CDN or proxy in front of WeKan sets
|
||||
(`models/lib/geoHeaders.js`). WeKan sends no address to a geolocation service,
|
||||
and the location is for display only - nothing is decided by it.
|
||||
|
||||
## Related files
|
||||
|
||||
| File | What it is |
|
||||
| --- | --- |
|
||||
| `models/lib/passwordPolicy.js` | The minimum, whether it is enforced, and which new passwords are refused. |
|
||||
| `models/lib/shortPasswordLogins.js` | What a row is, the filter, and how one more short sign-in updates a row. |
|
||||
| `models/shortPasswordLogins.js` | The collection. |
|
||||
| `server/lib/shortPasswordLogins.js` | Recording a short sign-in, marking it fixed, and one page of the table. |
|
||||
| `server/passwordChangeRequired.js` | The browser's report, the forced change, and signing every session out after a change. |
|
||||
| `client/components/settings/passwordLengthReport.*` | This page. |
|
||||
| `client/components/users/passwordChangeRequired.*` | **Change your password**, shown over everything while the change is required. |
|
||||
| `config/accounts.js` | Where the browser notes the length on the sign-in form and reports a short one. |
|
||||
| `tests/passwordPolicy.test.cjs`, `tests/passwordChangeRequired.test.cjs`, `tests/shortPasswordLogins.test.cjs` | Node tests. |
|
||||
| `tests/playwright/specs/password-min-length-signup-login.e2e.js`, `tests/playwright/specs/password-length-report.e2e.js` | Browser tests. |
|
||||
@@ -64,7 +64,7 @@ unsafe or noisy:
|
||||
failed or overdue backup is also listed on Summary
|
||||
([Backup.md](../../../Backup/Backup.md)).
|
||||
|
||||
**Reports** — Security Report, Impersonation Report, Performance, Speed, Tests, CPU
|
||||
**Reports** — Security Report, Impersonation Report, Password length, Performance, Speed, Tests, CPU
|
||||
usage, Instrumentation, Broken Cards, Files Report, Rules Report, Boards Report,
|
||||
Cards Report, Recovery, Offices, API, Database problems, Filesystem integrity and
|
||||
Email.
|
||||
@@ -127,6 +127,7 @@ the address uses: `/admin/problems/<slug>`.
|
||||
| Problems / Backup | `backup` | [Backup.md](../../../Backup/Backup.md#did-the-backup-work-history-health-and-monitoring) |
|
||||
| Problems / Security Report | `security-report` | — |
|
||||
| Problems / Impersonation Report | `impersonation` | — |
|
||||
| Problems / Password length | `password-length` | [Password-length.md](Password-length.md) |
|
||||
| Problems / Performance | `performance` | — |
|
||||
| Problems / Speed | `speed` | — |
|
||||
| Problems / Tests | `tests` | — |
|
||||
@@ -146,7 +147,7 @@ the address uses: `/admin/problems/<slug>`.
|
||||
| Problems / Filesystem integrity | `integrity` | — |
|
||||
| Problems / Email | `mail` | [Bounces.md](../../General/Email/Bounces.md#when-something-goes-wrong) |
|
||||
|
||||
15 of these 25 panes has no page of its own yet. A dash is a
|
||||
15 of these 26 panes has no page of its own yet. A dash is a
|
||||
gap to fill, not a pane that does nothing - what the pane shows is described in
|
||||
this README until somebody writes it up.
|
||||
|
||||
|
||||
@@ -100,6 +100,31 @@ the person signed in. See [Optional APIs](../../../API/Optional-APIs.md).
|
||||
automatically in card text, one per line. Anything not listed is left as plain
|
||||
text, which is what stops `javascript:` and friends becoming clickable.
|
||||
|
||||
Each listed scheme opens the application the viewer's computer has for it, so
|
||||
a link is something that runs on the machine of whoever clicks it, written by
|
||||
anybody who can edit a card. Two kinds of scheme are never linked, even when
|
||||
listed here:
|
||||
|
||||
- schemes that run code in the page or hide content in the URL: `javascript`,
|
||||
`vbscript`, `data`, `blob`, `about`, `view-source`, `mhtml` and the like;
|
||||
- operating-system handlers with a public code-execution history: `ms-msdt`
|
||||
(Follina, CVE-2022-30190), `search-ms` and `search`, `ms-officecmd`,
|
||||
`ms-appinstaller`, `ms-cxh` and `ms-cxh-full`, and `telnet` - the last step
|
||||
of a 2026 browser full chain, where Windows ran a `telnet.exe` from a
|
||||
directory any user can write to.
|
||||
|
||||
Somebody writing a link to one of the second kind in a card title or
|
||||
description, a comment or a checklist is shown in **Admin Panel → Problems**
|
||||
(HandlerLink, CWE-939): nobody needs such a link, because it is never
|
||||
clickable. The text is saved as written, and nothing is said to the writer.
|
||||
|
||||
`file` is allowed, because linking a shared folder is what the setting is for
|
||||
([#3218](https://github.com/wekan/wekan/issues/3218)). On Windows a
|
||||
`file://server/share` link is opened over SMB, and a link to a server
|
||||
outside your network sends that server the clicker's Windows sign-in (an NTLM
|
||||
hash). List `file` only where everybody who can edit cards is trusted, and
|
||||
block outbound SMB (TCP 445) at the network edge.
|
||||
|
||||
## Product name
|
||||
|
||||
The name WeKan calls itself: the browser tab title, and the branding on the sign-in
|
||||
|
||||
@@ -79,6 +79,7 @@ are not reachable from the current Admin Panel menu. Current storage panes use
|
||||
| Teams (`teams`) | [team.js](../../../models/team.js) | Team records and membership operations; scoped server permissions. |
|
||||
| People (`people`) | [users.js](../../../server/models/users.js) | Create/edit, roles, password/email operations, impersonation and removal. |
|
||||
| Locked Users (`locked-users`) | [lockedUsers.js](../../../server/methods/lockedUsers.js) | Locked-account listing and administrator unlock methods. |
|
||||
| Audit log (`audit-log`) | [adminAudit.js](../../../server/adminAudit.js) | New: every administrative method a site administrator calls, one document each for a year, secrets redacted; site administrators only. Real wrapper in [adminAudit.test.cjs](../../../tests/adminAudit.test.cjs). |
|
||||
| Roles (`roles`) | [inviteToBoardRolesSettings.js](../../../models/inviteToBoardRolesSettings.js) | Invitation-role defaults and per-role settings. |
|
||||
| Shared Templates (`shared-templates`) | [users.js](../../../server/models/users.js) | Shared-template administration with tenant boundaries. |
|
||||
|
||||
@@ -111,6 +112,7 @@ are not reachable from the current Admin Panel menu. Current storage panes use
|
||||
| Backup (`backup`) | [backup.js](../../../server/methods/backup.js) | Recorded backup runs, health of the instance backup and its history (backupHistory method); settings stay in Attachments / Backup. |
|
||||
| Security Report (`security-report`) | [eventLog.js](../../../models/eventLog.js) | Recorded security events, search, detail and acknowledgement. |
|
||||
| Impersonation Report (`impersonation`) | [impersonationReport.js](../../../server/publications/impersonationReport.js) | Scoped event publication and counts. |
|
||||
| Password length (`password-length`) | [shortPasswordLogins.js](../../../server/lib/shortPasswordLogins.js) | The minimum password length and whether it is enforced, saved as Login settings; one row per account that signed in with a shorter password, reported by its browser, with the Too short / Fixed / All filter. |
|
||||
| Performance (`performance`) | [adminProblems.js](../../../client/components/settings/adminProblems.js) | Persisted performance options; card loading uses the active loading policy. |
|
||||
| Speed (`speed`) | [eventLog.js](../../../models/eventLog.js) | Recorded speed events; no events is a valid empty report. |
|
||||
| Tests (`tests`) | [eventLog.js](../../../models/eventLog.js) | Recorded test results; opening the pane does not run every repository test. |
|
||||
|
||||
@@ -219,7 +219,7 @@ embedding in `.xlsx` is a file-format/library limitation rather than an omitted
|
||||
font asset; PDF is the format to use when identical glyph rendering on every
|
||||
device is required.
|
||||
|
||||
The design and progress live here rather than in `CHANGELOG.md`'s TODO list so
|
||||
The design and progress live here rather than in `CHANGELOG-big.md`'s TODO list so
|
||||
implementation details stay beside the format they describe.
|
||||
|
||||
## What is kept
|
||||
|
||||
@@ -148,7 +148,7 @@ KanbanFlow, Basecamp and Taskcafe. Restyaboard's CSV export is a closed paid
|
||||
app with undocumented columns. YouTrack, Airtable and Leantime write CSV
|
||||
headers that are undocumented or in the user's language. Things 3 exports only
|
||||
its SQLite database. Wrike's automation rules have no export, import or API.
|
||||
The **TODO Later** section of [CHANGELOG.md](../../../../../CHANGELOG.md) keeps the
|
||||
The **TODO Later** section of [CHANGELOG-big.md](../../../../../CHANGELOG-big.md) keeps the
|
||||
reason for each.
|
||||
|
||||
## REST API and `api.py`
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@
|
||||
|
||||
## Authentication
|
||||
|
||||
Most are these are already available at [Sandstorm](https://sandstorm.io), for example see
|
||||
Most are these are already available at [Sandstorm](https://sandstorm.org), for example see
|
||||
[screenshots of SAML support](https://discourse.wekan.io/t/sso-passing-variables-through-url/493/8).
|
||||
Following are for Source/Docker installs:
|
||||
|
||||
|
||||
@@ -174,6 +174,14 @@ whatever the entity. One document per change:
|
||||
Field snapshots preserve nested Date values with millisecond values and explicit
|
||||
`datePaths` metadata inside the content. This keeps date-valued custom fields
|
||||
as dates after JSON transport and restore, while ISO-looking text remains text.
|
||||
Each path is stored as a JSON string, `'["3","value"]'`, not as an array:
|
||||
FerretDB v1 does not store an array inside an array, and a row with
|
||||
`[["3","value"]]` was refused there and the change was not recorded (#6832).
|
||||
Rows written in the array form on MongoDB still restore, and Admin Panel →
|
||||
Attachments → database migration rewrites them to strings on the way to
|
||||
FerretDB. That migration counts every document the target refuses and
|
||||
reports the run as failed, with the count per collection, instead of
|
||||
success.
|
||||
Malformed paths or values are rejected instead of coercing data during restore.
|
||||
Older rows without this metadata keep their original interpretation: dates
|
||||
already flattened to strings cannot safely be distinguished from actual text.
|
||||
|
||||
@@ -63,6 +63,7 @@ Menu path is what you click to reach the page.
|
||||
| Domains | Admin Panel / People / Domains | Every e-mail domain in use with the number of users on it. The first People pane converted to this design. |
|
||||
| Roles Status | Admin Panel / People / Roles | What each board role may do — which cards it sees, and whether it may comment, create and edit, or change the board's settings. **Read-only**: no `rowTemplate`, no actions, nothing editable, because a role's capabilities are a property of the code and not a setting. It sits BELOW the Save button of the checkbox list above it and follows that list's working copy, so an admin sees what a change means before saving it. Rendered from `models/lib/boardRoleCapabilities.js`, the same table the allow rules decide with — see [Board roles](../Board-Sidebar/Members/Roles.md). |
|
||||
| Security | Admin Panel / Problems / Security | Security events from the event log: blocked uploads, rejected URL schemes, auth failures. One row per event, newest first. |
|
||||
| Password length | Admin Panel / Problems / Password length | Accounts that signed in with a password shorter than the minimum: username, first and last such login, the latest IPv4 and IPv6 address each with its flag and city when known, the count, and when it was fixed. Its filter is the status - Too short password (the default), Fixed to long enough password, All - and above the table are the two settings, the minimum length and whether it is enforced. See [Password length](../Admin-Panel/Problems/Password-length.md). |
|
||||
| Speed | Admin Panel / Problems / Speed | Slow-operation events — what took too long, where, and for how long. |
|
||||
| Tests | Admin Panel / Problems / Tests | Test-run events recorded on the server. |
|
||||
| CPU usage | Admin Panel / Problems / CPU usage | Past high-CPU periods. Adds a **status row**: the live CPU percent, core count and load average, because the table itself is history, not the current state. |
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
**a) Wekan Snap**
|
||||
```
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan/current/lib/x86_64-linux-gnu
|
||||
export PATH="$PATH:/snap/wekan/current/bin"
|
||||
S=/snap; [ -d /snap/wekan ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan/current/lib/$(uname -m)-linux-gnu"
|
||||
export PATH="$PATH:$S/wekan/current/bin"
|
||||
mongo --port 27019
|
||||
```
|
||||
If you have disabled new user registration at Admin Panel, you can enable it, and create new user your https://wekan.example.com/sign-up :
|
||||
@@ -34,8 +35,9 @@ Then login to Wekan and change any users passwords at `Admin Panel / People / Pe
|
||||
|
||||
**b) Wekan Gantt GPL Snap**
|
||||
```
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/snap/wekan-gantt-gpl/current/lib/x86_64-linux-gnu
|
||||
export PATH="$PATH:/snap/wekan-gantt-gpl/current/bin"
|
||||
S=/snap; [ -d /snap/wekan-gantt-gpl ] || S=/var/lib/snapd/snap
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}:$S/wekan-gantt-gpl/current/lib/$(uname -m)-linux-gnu"
|
||||
export PATH="$PATH:$S/wekan-gantt-gpl/current/bin"
|
||||
mongo --port 27019
|
||||
```
|
||||
If you have disabled new user registration at Admin Panel, you can enable it, and create new user your https://wekan.example.com/sign-up :
|
||||
|
||||
@@ -67,4 +67,4 @@ sudo systemclt start proxybouncer
|
||||
```
|
||||
Question: Does this setup imply that everyone will be logged in to Wekan as 'proxybouncer'? Is there a way to pass username from Google via headers, etc.?
|
||||
|
||||
Answer: First login to Proxybouncer can limit login domain of G Suite. Second login is using Wekan username and password. There is no integrated login yet for standalone Wekan like there is for https://sandstorm.io
|
||||
Answer: First login to Proxybouncer can limit login domain of G Suite. Second login is using Wekan username and password. There is no integrated login yet for standalone Wekan like there is for https://sandstorm.org
|
||||
@@ -122,5 +122,5 @@ coverage, not native-speaker verification; specialist terminology still needs
|
||||
review. Chromium coverage exercises Gujarati block dragging,
|
||||
field editing, saving and context menus. The editor
|
||||
remains available; remaining languages and native terminology review are
|
||||
tracked under TODO Later in [the changelog](../../../../CHANGELOG.md).
|
||||
tracked under TODO Later in [the changelog](../../../../CHANGELOG-big.md).
|
||||
Message coverage and placeholder checks do not imply complete translation.
|
||||
@@ -54,12 +54,12 @@ From Devices / Install Guest Additions CD image...
|
||||
|
||||
## 6) Install Sandstorm dev version
|
||||
|
||||
[Info source](https://sandstorm.io/install)
|
||||
[Info source](https://sandstorm.org/install)
|
||||
|
||||
Start install:
|
||||
|
||||
```
|
||||
curl https://install.sandstorm.io | bash
|
||||
curl https://install.sandstorm.org | bash
|
||||
```
|
||||
|
||||
Use options for development / dev install.
|
||||
@@ -85,7 +85,7 @@ sudo useradd -G sandstorm wekan
|
||||
|
||||
```
|
||||
cd ~/repos
|
||||
curl https://dl.sandstorm.io/meteor-spk-0.4.0.tar.xz | tar Jxf -
|
||||
curl https://dl.sandstorm.org/meteor-spk-0.4.0.tar.xz | tar Jxf -
|
||||
echo "export PATH=$PATH:~/repos/meteor-spk-0.4.0" >> ~/.bashrc
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Read:
|
||||
|
||||
Start install:
|
||||
```
|
||||
curl https://install.sandstorm.io | bash
|
||||
curl https://install.sandstorm.org | bash
|
||||
```
|
||||
|
||||
Use options for development / dev install.
|
||||
@@ -38,7 +38,7 @@ sudo sandstorm start
|
||||
```
|
||||
mkdir ~/repos
|
||||
cd ~/repos
|
||||
curl https://dl.sandstorm.io/meteor-spk-0.4.0.tar.xz | tar Jxf -
|
||||
curl https://dl.sandstorm.org/meteor-spk-0.4.0.tar.xz | tar Jxf -
|
||||
echo "export PATH=$PATH:~/repos/meteor-spk-0.4.0" >> ~/.bashrc
|
||||
```
|
||||
|
||||
@@ -122,7 +122,7 @@ meteor-spk dev
|
||||
|
||||
`App in now available from Sandstorm server. Ctrl-C to disconnect.`
|
||||
|
||||
With Firefox/Chromium/Chrome/Edge/Chromium Edge browser, open http://local.sandstorm.io:6080/
|
||||
With Firefox/Chromium/Chrome/Edge/Chromium Edge browser, open http://local.sandstorm.org:6080/
|
||||
|
||||
Login with premade local dev account Alice.
|
||||
|
||||
|
||||
Loaded 100 of 651 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user