Compare commits

...

20 Commits

Author SHA1 Message Date
Mathijs van Veluw
36f0620fd1 Fix org-details issue (#6811)
Fix an issue where it was possible for users who were not eligible to access all org ciphers to be able to download and extract the encrypted contents.
Only Managers with full access and Admins and Owners should be able to access this endpoint.

This change will block and prevent access for other users.

Signed-off-by: BlackDex <black.dex@gmail.com>
2026-02-10 20:34:30 +01:00
Mathijs van Veluw
3cd2d4afe7 Update crates and web-vault (#6810)
Signed-off-by: BlackDex <black.dex@gmail.com>
2026-02-10 20:24:35 +01:00
Mathijs van Veluw
d09c45bb63 Misc updates, crates, rust, js, gha, vault (#6799) 2026-02-08 19:24:20 +01:00
Stefan Melmuk
feecfb20da fix error message for purging auth requests (#6776) 2026-02-01 22:35:55 +01:00
Timshel
347279a12c Empty AccountKeys when no private key (#6761)
Co-authored-by: Timshel <timshel@users.noreply.github.com>
2026-02-01 22:35:22 +01:00
Helmut K. C. Tessarek
7f65a254b3 refactor: improve tooltips in diagnostics page (#6765)
The term "seems to" is used too loosely in many of the tooltips, but in
these 2 instances it is wrong wording.
An update is either available or not. If there is no update, one could
argue that "seems to" is valid, since the Internet could be down to
check for a new version. But in this situation the update is availble.
It is impossible that an update seems to be available.
2026-02-01 22:35:03 +01:00
Mathijs van Veluw
cc80f689ed Update crates, web-vault, js, workflows (#6749)
- Updated all crates
- Updated web-vault to v2025.12.2
- Updated all JavaScript files
- Updated all GitHub Action Workflows
  Also added the `concurrency` option to all workflows.

Signed-off-by: BlackDex <black.dex@gmail.com>
2026-01-22 23:40:39 +01:00
Stefan Melmuk
4737192853 fix email as 2fa with auth requests (#6736)
* fix email as 2fa with auth requests

* increase expiry time of auth_requests to 15 minutes
2026-01-22 23:25:11 +01:00
Stefan Melmuk
0c6817cb4e hide password hints via CSS (#6726) 2026-01-18 15:25:20 +01:00
Stefan Melmuk
25a71d913f use email instead of empty name for webauhn (#6733)
* if empty use email instead of name for webauhn

* use email as display name if name is empty
2026-01-18 15:23:21 +01:00
Mathijs van Veluw
b2cd556f3e Fix User API Key login (#6712)
When using the latest Bitwarden CLI and logging in using the API Key, it expects some extra fields, same as for normal login.
This PR adds those fields and login is possible again via API Key.

Fixes #6709

Signed-off-by: BlackDex <black.dex@gmail.com>
2026-01-14 13:11:43 +01:00
Mathijs van Veluw
4352fffeec Fix web-vault version check and update web-vault (#6686) 2026-01-09 13:21:10 +01:00
Stefan Melmuk
8d08697cf8 improve sso callback path (#6676)
* normalize base_url for sso_callback_path

* clean url when embedding images
2026-01-06 17:10:00 +00:00
Stefan Melmuk
9f1df42259 allow MasterPasswordHash for Android (#6673) 2026-01-06 14:24:05 +00:00
Stefan Melmuk
1e1f9957cd return no content with status code 204 (#6665) 2026-01-05 18:52:24 +00:00
Stefan Melmuk
bf37657c08 update web-vault to fix org creation (#6646) 2026-01-01 16:52:11 +00:00
Daniel García
3e2cef7e8b Try old refresh token if we fail to decode jwt (#6629) 2025-12-29 22:54:51 +01:00
Mathijs van Veluw
2af9d21158 Misc updates (#6627)
- Update crates and toml
- Update web-vault to v2025.12.1
- Update workflows

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-12-29 22:27:12 +01:00
Daniel
c4f6c4e63b Re-add alpine tag (#6626)
- fixes https://github.com/dani-garcia/vaultwarden/issues/6619
- also optimize the process while at it
2025-12-29 22:25:15 +01:00
Daniel García
eb2a56aea1 Update lockfile (#6600) 2025-12-28 01:07:17 +01:00
40 changed files with 4544 additions and 5926 deletions

View File

@@ -1,6 +1,10 @@
name: Build name: Build
permissions: {} permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on: on:
push: push:
paths: paths:
@@ -30,6 +34,10 @@ on:
- "docker/DockerSettings.yaml" - "docker/DockerSettings.yaml"
- "macros/**" - "macros/**"
defaults:
run:
shell: bash
jobs: jobs:
build: build:
name: Build and Test ${{ matrix.channel }} name: Build and Test ${{ matrix.channel }}
@@ -63,7 +71,6 @@ jobs:
# Determine rust-toolchain version # Determine rust-toolchain version
- name: Init Variables - name: Init Variables
id: toolchain id: toolchain
shell: bash
env: env:
CHANNEL: ${{ matrix.channel }} CHANNEL: ${{ matrix.channel }}
run: | run: |

View File

@@ -1,8 +1,16 @@
name: Check templates name: Check templates
permissions: {} permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on: [ push, pull_request ] on: [ push, pull_request ]
defaults:
run:
shell: bash
jobs: jobs:
docker-templates: docker-templates:
name: Validate docker templates name: Validate docker templates

View File

@@ -1,8 +1,15 @@
name: Hadolint name: Hadolint
on: [ push, pull_request ]
permissions: {} permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on: [ push, pull_request ]
defaults:
run:
shell: bash
jobs: jobs:
hadolint: hadolint:
@@ -25,7 +32,6 @@ jobs:
# Download hadolint - https://github.com/hadolint/hadolint/releases # Download hadolint - https://github.com/hadolint/hadolint/releases
- name: Download hadolint - name: Download hadolint
shell: bash
run: | run: |
sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \ sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
sudo chmod +x /usr/local/bin/hadolint sudo chmod +x /usr/local/bin/hadolint
@@ -41,13 +47,11 @@ jobs:
# Test Dockerfiles with hadolint # Test Dockerfiles with hadolint
- name: Run hadolint - name: Run hadolint
shell: bash
run: hadolint docker/Dockerfile.{debian,alpine} run: hadolint docker/Dockerfile.{debian,alpine}
# End Test Dockerfiles with hadolint # End Test Dockerfiles with hadolint
# Test Dockerfiles with docker build checks # Test Dockerfiles with docker build checks
- name: Run docker build check - name: Run docker build check
shell: bash
run: | run: |
echo "Checking docker/Dockerfile.debian" echo "Checking docker/Dockerfile.debian"
docker build --check . -f docker/Dockerfile.debian docker build --check . -f docker/Dockerfile.debian

View File

@@ -1,6 +1,12 @@
name: Release name: Release
permissions: {} permissions: {}
concurrency:
# Apply concurrency control only on the upstream repo
group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}
# Don't cancel other runs when creating a tag
cancel-in-progress: ${{ github.ref_type == 'branch' }}
on: on:
push: push:
branches: branches:
@@ -10,12 +16,6 @@ on:
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
- '[1-2].[0-9]+.[0-9]+' - '[1-2].[0-9]+.[0-9]+'
concurrency:
# Apply concurrency control only on the upstream repo
group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}
# Don't cancel other runs when creating a tag
cancel-in-progress: ${{ github.ref_type == 'branch' }}
defaults: defaults:
run: run:
shell: bash shell: bash
@@ -102,7 +102,7 @@ jobs:
# Login to Docker Hub # Login to Docker Hub
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -117,7 +117,7 @@ jobs:
# Login to GitHub Container Registry # Login to GitHub Container Registry
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -133,7 +133,7 @@ jobs:
# Login to Quay.io # Login to Quay.io
- name: Login to Quay.io - name: Login to Quay.io
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
@@ -233,7 +233,7 @@ jobs:
# Upload artifacts to Github Actions and Attest the binaries # Upload artifacts to Github Actions and Attest the binaries
- name: Attest binaries - name: Attest binaries
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
with: with:
subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }} subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }}
@@ -265,7 +265,7 @@ jobs:
# Login to Docker Hub # Login to Docker Hub
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -280,7 +280,7 @@ jobs:
# Login to GitHub Container Registry # Login to GitHub Container Registry
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -296,7 +296,7 @@ jobs:
# Login to Quay.io # Login to Quay.io
- name: Login to Quay.io - name: Login to Quay.io
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
@@ -313,45 +313,43 @@ jobs:
# Determine Base Tags # Determine Base Tags
- name: Determine Base Tags - name: Determine Base Tags
env: env:
BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}"
REF_TYPE: ${{ github.ref_type }} REF_TYPE: ${{ github.ref_type }}
run: | run: |
# Check which main tag we are going to build determined by ref_type # Check which main tag we are going to build determined by ref_type
if [[ "${REF_TYPE}" == "tag" ]]; then if [[ "${REF_TYPE}" == "tag" ]]; then
echo "BASE_TAGS=latest,${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_ENV}" echo "BASE_TAGS=latest${BASE_IMAGE_TAG},${GITHUB_REF#refs/*/}${BASE_IMAGE_TAG}${BASE_IMAGE_TAG//-/,}" | tee -a "${GITHUB_ENV}"
elif [[ "${REF_TYPE}" == "branch" ]]; then elif [[ "${REF_TYPE}" == "branch" ]]; then
echo "BASE_TAGS=testing" | tee -a "${GITHUB_ENV}" echo "BASE_TAGS=testing${BASE_IMAGE_TAG}" | tee -a "${GITHUB_ENV}"
fi fi
- name: Create manifest list, push it and extract digest SHA - name: Create manifest list, push it and extract digest SHA
working-directory: ${{ runner.temp }}/digests working-directory: ${{ runner.temp }}/digests
env: env:
BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}"
BASE_TAGS: "${{ env.BASE_TAGS }}" BASE_TAGS: "${{ env.BASE_TAGS }}"
CONTAINER_REGISTRIES: "${{ env.CONTAINER_REGISTRIES }}" CONTAINER_REGISTRIES: "${{ env.CONTAINER_REGISTRIES }}"
run: | run: |
set +e
IFS=',' read -ra IMAGES <<< "${CONTAINER_REGISTRIES}" IFS=',' read -ra IMAGES <<< "${CONTAINER_REGISTRIES}"
IFS=',' read -ra TAGS <<< "${BASE_TAGS}" IFS=',' read -ra TAGS <<< "${BASE_TAGS}"
TAG_ARGS=()
for img in "${IMAGES[@]}"; do for img in "${IMAGES[@]}"; do
for tag in "${TAGS[@]}"; do for tag in "${TAGS[@]}"; do
echo "Creating manifest for ${img}:${tag}${BASE_IMAGE_TAG}" TAG_ARGS+=("-t" "${img}:${tag}")
OUTPUT=$(docker buildx imagetools create \
-t "${img}:${tag}${BASE_IMAGE_TAG}" \
$(printf "${img}@sha256:%s " *) 2>&1)
STATUS=$?
if [ ${STATUS} -ne 0 ]; then
echo "Manifest creation failed for ${img}:${tag}${BASE_IMAGE_TAG}"
echo "${OUTPUT}"
exit ${STATUS}
fi
echo "Manifest created for ${img}:${tag}${BASE_IMAGE_TAG}"
echo "${OUTPUT}"
done done
done done
set -e
echo "Creating manifest"
if ! OUTPUT=$(docker buildx imagetools create \
"${TAG_ARGS[@]}" \
$(printf "${IMAGES[0]}@sha256:%s " *) 2>&1); then
echo "Manifest creation failed"
echo "${OUTPUT}"
exit 1
fi
echo "Manifest created successfully"
echo "${OUTPUT}"
# Extract digest SHA for subsequent steps # Extract digest SHA for subsequent steps
GET_DIGEST_SHA="$(echo "${OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)" GET_DIGEST_SHA="$(echo "${OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"
@@ -360,7 +358,7 @@ jobs:
# Attest container images # Attest container images
- name: Attest - docker.io - ${{ matrix.base_image }} - name: Attest - docker.io - ${{ matrix.base_image }}
if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && env.DIGEST_SHA != ''}} if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && env.DIGEST_SHA != ''}}
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
with: with:
subject-name: ${{ vars.DOCKERHUB_REPO }} subject-name: ${{ vars.DOCKERHUB_REPO }}
subject-digest: ${{ env.DIGEST_SHA }} subject-digest: ${{ env.DIGEST_SHA }}
@@ -368,7 +366,7 @@ jobs:
- name: Attest - ghcr.io - ${{ matrix.base_image }} - name: Attest - ghcr.io - ${{ matrix.base_image }}
if: ${{ env.HAVE_GHCR_LOGIN == 'true' && env.DIGEST_SHA != ''}} if: ${{ env.HAVE_GHCR_LOGIN == 'true' && env.DIGEST_SHA != ''}}
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
with: with:
subject-name: ${{ vars.GHCR_REPO }} subject-name: ${{ vars.GHCR_REPO }}
subject-digest: ${{ env.DIGEST_SHA }} subject-digest: ${{ env.DIGEST_SHA }}
@@ -376,7 +374,7 @@ jobs:
- name: Attest - quay.io - ${{ matrix.base_image }} - name: Attest - quay.io - ${{ matrix.base_image }}
if: ${{ env.HAVE_QUAY_LOGIN == 'true' && env.DIGEST_SHA != ''}} if: ${{ env.HAVE_QUAY_LOGIN == 'true' && env.DIGEST_SHA != ''}}
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
with: with:
subject-name: ${{ vars.QUAY_REPO }} subject-name: ${{ vars.QUAY_REPO }}
subject-digest: ${{ env.DIGEST_SHA }} subject-digest: ${{ env.DIGEST_SHA }}

View File

@@ -1,6 +1,10 @@
name: Cleanup name: Cleanup
permissions: {} permissions: {}
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:

View File

@@ -1,6 +1,10 @@
name: Trivy name: Trivy
permissions: {} permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on: on:
push: push:
branches: branches:
@@ -46,6 +50,6 @@ jobs:
severity: CRITICAL,HIGH severity: CRITICAL,HIGH
- name: Upload Trivy scan results to GitHub Security tab - name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
with: with:
sarif_file: 'trivy-results.sarif' sarif_file: 'trivy-results.sarif'

View File

@@ -1,7 +1,11 @@
name: Code Spell Checking name: Code Spell Checking
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on: [ push, pull_request ] on: [ push, pull_request ]
permissions: {}
jobs: jobs:
typos: typos:
@@ -19,4 +23,4 @@ jobs:
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
- name: Spell Check Repo - name: Spell Check Repo
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0 uses: crate-ci/typos@9066e9940a8a05b98fb4733c62a726f83c9e57f8 # v1.43.3

View File

@@ -1,4 +1,9 @@
name: Security Analysis with zizmor name: Security Analysis with zizmor
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on: on:
push: push:
@@ -6,8 +11,6 @@ on:
pull_request: pull_request:
branches: ["**"] branches: ["**"]
permissions: {}
jobs: jobs:
zizmor: zizmor:
name: Run zizmor name: Run zizmor
@@ -21,7 +24,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
with: with:
# intentionally not scanning the entire repository, # intentionally not scanning the entire repository,
# since it contains integration tests. # since it contains integration tests.

View File

@@ -53,6 +53,6 @@ repos:
- "cd docker && make" - "cd docker && make"
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: 2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0 rev: 9066e9940a8a05b98fb4733c62a726f83c9e57f8 # v1.43.3
hooks: hooks:
- id: typos - id: typos

734
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[workspace.package] [workspace.package]
edition = "2021" edition = "2021"
rust-version = "1.90.0" rust-version = "1.91.0"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
repository = "https://github.com/dani-garcia/vaultwarden" repository = "https://github.com/dani-garcia/vaultwarden"
publish = false publish = false
@@ -65,30 +65,30 @@ dotenvy = { version = "0.15.7", default-features = false }
# Numerical libraries # Numerical libraries
num-traits = "0.2.19" num-traits = "0.2.19"
num-derive = "0.4.2" num-derive = "0.4.2"
bigdecimal = "0.4.9" bigdecimal = "0.4.10"
# Web framework # Web framework
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false } rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
rocket_ws = { version ="0.1.1" } rocket_ws = { version ="0.1.1" }
# WebSockets libraries # WebSockets libraries
rmpv = "1.3.0" # MessagePack library rmpv = "1.3.1" # MessagePack library
# Concurrent HashMap used for WebSocket messaging and favicons # Concurrent HashMap used for WebSocket messaging and favicons
dashmap = "6.1.0" dashmap = "6.1.0"
# Async futures # Async futures
futures = "0.3.31" futures = "0.3.31"
tokio = { version = "1.48.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio = { version = "1.49.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
tokio-util = { version = "0.7.17", features = ["compat"]} tokio-util = { version = "0.7.18", features = ["compat"]}
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145" serde_json = "1.0.149"
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
# Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility # Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility
diesel = { version = "2.3.5", features = ["chrono", "r2d2", "numeric"] } diesel = { version = "2.3.6", features = ["chrono", "r2d2", "numeric"] }
diesel_migrations = "2.3.1" diesel_migrations = "2.3.1"
derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] } derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] }
@@ -103,21 +103,21 @@ ring = "0.17.14"
subtle = "2.6.1" subtle = "2.6.1"
# UUID generation # UUID generation
uuid = { version = "1.19.0", features = ["v4"] } uuid = { version = "1.20.0", features = ["v4"] }
# Date and time libraries # Date and time libraries
chrono = { version = "0.4.42", features = ["clock", "serde"], default-features = false } chrono = { version = "0.4.43", features = ["clock", "serde"], default-features = false }
chrono-tz = "0.10.4" chrono-tz = "0.10.4"
time = "0.3.44" time = "0.3.47"
# Job scheduler # Job scheduler
job_scheduler_ng = "2.4.0" job_scheduler_ng = "2.4.0"
# Data encoding library Hex/Base32/Base64 # Data encoding library Hex/Base32/Base64
data-encoding = "2.9.0" data-encoding = "2.10.0"
# JWT library # JWT library
jsonwebtoken = { version = "10.2.0", features = ["use_pem", "rust_crypto"], default-features = false } jsonwebtoken = { version = "10.3.0", features = ["use_pem", "rust_crypto"], default-features = false }
# TOTP library # TOTP library
totp-lite = "2.0.1" totp-lite = "2.0.1"
@@ -133,7 +133,7 @@ webauthn-rs-proto = "0.5.4"
webauthn-rs-core = "0.5.4" webauthn-rs-core = "0.5.4"
# Handling of URL's for WebAuthn and favicons # Handling of URL's for WebAuthn and favicons
url = "2.5.7" url = "2.5.8"
# Email libraries # Email libraries
lettre = { version = "0.11.19", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } lettre = { version = "0.11.19", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
@@ -141,7 +141,7 @@ percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails
email_address = "0.2.9" email_address = "0.2.9"
# HTML Template library # HTML Template library
handlebars = { version = "6.3.2", features = ["dir_source"] } handlebars = { version = "6.4.0", features = ["dir_source"] }
# HTTP client (Used for favicons, version check, DUO and HIBP API) # HTTP client (Used for favicons, version check, DUO and HIBP API)
reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false} reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
@@ -149,9 +149,9 @@ hickory-resolver = "0.25.2"
# Favicon extraction libraries # Favicon extraction libraries
html5gum = "0.8.3" html5gum = "0.8.3"
regex = { version = "1.12.2", features = ["std", "perf", "unicode-perl"], default-features = false } regex = { version = "1.12.3", features = ["std", "perf", "unicode-perl"], default-features = false }
data-url = "0.3.2" data-url = "0.3.2"
bytes = "1.11.0" bytes = "1.11.1"
svg-hush = "0.9.5" svg-hush = "0.9.5"
# Cache function results (Used for version check and favicon fetching) # Cache function results (Used for version check and favicon fetching)
@@ -197,10 +197,10 @@ grass_compiler = { version = "0.13.4", default-features = false }
opendal = { version = "0.55.0", features = ["services-fs"], default-features = false } opendal = { version = "0.55.0", features = ["services-fs"], default-features = false }
# For retrieving AWS credentials, including temporary SSO credentials # For retrieving AWS credentials, including temporary SSO credentials
anyhow = { version = "1.0.100", optional = true } anyhow = { version = "1.0.101", optional = true }
aws-config = { version = "1.8.12", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } aws-config = { version = "1.8.13", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
aws-credential-types = { version = "1.2.11", optional = true } aws-credential-types = { version = "1.2.11", optional = true }
aws-smithy-runtime-api = { version = "1.9.3", optional = true } aws-smithy-runtime-api = { version = "1.11.3", optional = true }
http = { version = "1.4.0", optional = true } http = { version = "1.4.0", optional = true }
reqsign = { version = "0.16.5", optional = true } reqsign = { version = "0.16.5", optional = true }

View File

@@ -1,11 +1,11 @@
--- ---
vault_version: "v2025.12.0" vault_version: "v2026.1.1"
vault_image_digest: "sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613" vault_image_digest: "sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7"
# Cross Compile Docker Helper Scripts v1.9.0 # Cross Compile Docker Helper Scripts v1.9.0
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707"
rust_version: 1.92.0 # Rust version to be used rust_version: 1.93.0 # Rust version to be used
debian_version: trixie # Debian release name to be used debian_version: trixie # Debian release name to be used
alpine_version: "3.23" # Alpine version to be used alpine_version: "3.23" # Alpine version to be used
# For which platforms/architectures will we try to build images # For which platforms/architectures will we try to build images

View File

@@ -19,23 +19,23 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.0 # $ docker pull docker.io/vaultwarden/web-vault:v2026.1.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.1
# [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613] # [docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7
# [docker.io/vaultwarden/web-vault:v2025.12.0] # [docker.io/vaultwarden/web-vault:v2026.1.1]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 AS vault
########################## ALPINE BUILD IMAGES ########################## ########################## ALPINE BUILD IMAGES ##########################
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64
## And for Alpine we define all build images here, they will only be loaded when actually used ## And for Alpine we define all build images here, they will only be loaded when actually used
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.92.0 AS build_amd64 FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.93.0 AS build_amd64
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.92.0 AS build_arm64 FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.93.0 AS build_arm64
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.92.0 AS build_armv7 FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.93.0 AS build_armv7
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.92.0 AS build_armv6 FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.93.0 AS build_armv6
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006 # hadolint ignore=DL3006

View File

@@ -19,15 +19,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.0 # $ docker pull docker.io/vaultwarden/web-vault:v2026.1.1
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.1
# [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613] # [docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7
# [docker.io/vaultwarden/web-vault:v2025.12.0] # [docker.io/vaultwarden/web-vault:v2026.1.1]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 AS vault
########################## Cross Compile Docker Helper Scripts ########################## ########################## Cross Compile Docker Helper Scripts ##########################
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
@@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006 # hadolint ignore=DL3006
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.92.0-slim-trixie AS build FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.93.0-slim-trixie AS build
COPY --from=xx / / COPY --from=xx / /
ARG TARGETARCH ARG TARGETARCH
ARG TARGETVARIANT ARG TARGETVARIANT

View File

@@ -19,13 +19,13 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
# click the tag name to view the digest of the image it currently points to. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version }} # $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version }} # $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}
# [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}] # [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }} # $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
# [docker.io/vaultwarden/web-vault:{{ vault_version }}] # [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault

View File

@@ -13,8 +13,8 @@ path = "src/lib.rs"
proc-macro = true proc-macro = true
[dependencies] [dependencies]
quote = "1.0.42" quote = "1.0.44"
syn = "2.0.111" syn = "2.0.114"
[lints] [lints]
workspace = true workspace = true

View File

@@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "1.92.0" channel = "1.93.0"
components = [ "rustfmt", "clippy" ] components = [ "rustfmt", "clippy" ]
profile = "minimal" profile = "minimal"

View File

@@ -31,7 +31,7 @@ use crate::{
http_client::make_http_request, http_client::make_http_request,
mail, mail,
util::{ util::{
container_base_image, format_naive_datetime_local, get_display_size, get_web_vault_version, container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size,
is_running_in_container, NumberOrString, is_running_in_container, NumberOrString,
}, },
CONFIG, VERSION, CONFIG, VERSION,
@@ -689,6 +689,26 @@ async fn get_ntp_time(has_http_access: bool) -> String {
String::from("Unable to fetch NTP time.") String::from("Unable to fetch NTP time.")
} }
fn web_vault_compare(active: &str, latest: &str) -> i8 {
use semver::Version;
use std::cmp::Ordering;
let active_semver = Version::parse(active).unwrap_or_else(|e| {
warn!("Unable to parse active web-vault version '{active}': {e}");
Version::parse("2025.1.1").unwrap()
});
let latest_semver = Version::parse(latest).unwrap_or_else(|e| {
warn!("Unable to parse latest web-vault version '{latest}': {e}");
Version::parse("2025.1.1").unwrap()
});
match active_semver.cmp(&latest_semver) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}
}
#[get("/diagnostics")] #[get("/diagnostics")]
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> { async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
use chrono::prelude::*; use chrono::prelude::*;
@@ -708,32 +728,21 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
_ => "Unable to resolve domain name.".to_string(), _ => "Unable to resolve domain name.".to_string(),
}; };
let (latest_release, latest_commit, latest_web_build) = get_release_info(has_http_access).await; let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await;
let active_web_release = get_active_web_release();
let web_vault_compare = web_vault_compare(&active_web_release, &latest_web_release);
let ip_header_name = &ip_header.0.unwrap_or_default(); let ip_header_name = &ip_header.0.unwrap_or_default();
// Get current running versions
let web_vault_version = get_web_vault_version();
// Check if the running version is newer than the latest stable released version
let web_vault_pre_release = if let Ok(web_ver_match) = semver::VersionReq::parse(&format!(">{latest_web_build}")) {
web_ver_match.matches(
&semver::Version::parse(&web_vault_version).unwrap_or_else(|_| semver::Version::parse("2025.1.1").unwrap()),
)
} else {
error!("Unable to parse latest_web_build: '{latest_web_build}'");
false
};
let diagnostics_json = json!({ let diagnostics_json = json!({
"dns_resolved": dns_resolved, "dns_resolved": dns_resolved,
"current_release": VERSION, "current_release": VERSION,
"latest_release": latest_release, "latest_release": latest_vw_release,
"latest_commit": latest_commit, "latest_commit": latest_vw_commit,
"web_vault_enabled": &CONFIG.web_vault_enabled(), "web_vault_enabled": &CONFIG.web_vault_enabled(),
"web_vault_version": web_vault_version, "active_web_release": active_web_release,
"latest_web_build": latest_web_build, "latest_web_release": latest_web_release,
"web_vault_pre_release": web_vault_pre_release, "web_vault_compare": web_vault_compare,
"running_within_container": running_within_container, "running_within_container": running_within_container,
"container_base_image": if running_within_container { container_base_image() } else { "Not applicable" }, "container_base_image": if running_within_container { container_base_image() } else { "Not applicable" },
"has_http_access": has_http_access, "has_http_access": has_http_access,
@@ -844,3 +853,32 @@ impl<'r> FromRequest<'r> for AdminToken {
}) })
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_web_vault_compare() {
// web_vault_compare(active, latest)
// Test normal versions
assert!(web_vault_compare("2025.12.0", "2025.12.1") == -1);
assert!(web_vault_compare("2025.12.1", "2025.12.1") == 0);
assert!(web_vault_compare("2025.12.2", "2025.12.1") == 1);
// Test patched/+build.n versions
// Newer latest version
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1") == -1);
assert!(web_vault_compare("2025.12.1", "2025.12.1+build.1") == -1);
assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1") == -1);
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2") == -1);
// Equal versions
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1") == 0);
assert!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2") == 0);
// Newer active version
assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1") == 1);
assert!(web_vault_compare("2025.12.2", "2025.12.1+build.1") == 1);
assert!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1") == 1);
assert!(web_vault_compare("2025.12.1+build.3", "2025.12.1+build.2") == 1);
}
}

View File

@@ -1704,6 +1704,6 @@ pub async fn purge_auth_requests(pool: DbPool) {
if let Ok(conn) = pool.get().await { if let Ok(conn) = pool.get().await {
AuthRequest::purge_expired_auth_requests(&conn).await; AuthRequest::purge_expired_auth_requests(&conn).await;
} else { } else {
error!("Failed to get DB connection while purging trashed ciphers") error!("Failed to get DB connection while purging auth requests")
} }
} }

View File

@@ -929,11 +929,15 @@ struct OrgIdData {
} }
#[get("/ciphers/organization-details?<data..>")] #[get("/ciphers/organization-details?<data..>")]
async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { async fn get_org_details(data: OrgIdData, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
if data.organization_id != headers.membership.org_uuid { if data.organization_id != headers.membership.org_uuid {
err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code); err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code);
} }
if !headers.membership.has_full_access() {
err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code);
}
Ok(Json(json!({ Ok(Json(json!({
"data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await?, "data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await?,
"object": "list", "object": "list",
@@ -3207,7 +3211,7 @@ async fn put_reset_password(
// Sending email before resetting password to ensure working email configuration and the resulting // Sending email before resetting password to ensure working email configuration and the resulting
// user notification. Also this might add some protection against security flaws and misuse // user notification. Also this might add some protection against security flaws and misuse
if let Err(e) = mail::send_admin_reset_password(&user.email, &user.name, &org.name).await { if let Err(e) = mail::send_admin_reset_password(&user.email, user.display_name(), &org.name).await {
err!(format!("Error sending user reset password email: {e:#?}")); err!(format!("Error sending user reset password email: {e:#?}"));
} }

View File

@@ -7,10 +7,10 @@ use crate::{
core::{log_user_event, two_factor::_generate_recover_code}, core::{log_user_event, two_factor::_generate_recover_code},
EmptyResult, JsonResult, PasswordOrOtpData, EmptyResult, JsonResult, PasswordOrOtpData,
}, },
auth::Headers, auth::{ClientHeaders, Headers},
crypto, crypto,
db::{ db::{
models::{DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId}, models::{AuthRequest, AuthRequestId, DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId},
DbConn, DbConn,
}, },
error::{Error, MapResult}, error::{Error, MapResult},
@@ -30,12 +30,14 @@ struct SendEmailLoginData {
email: Option<String>, email: Option<String>,
#[serde(alias = "MasterPasswordHash")] #[serde(alias = "MasterPasswordHash")]
master_password_hash: Option<String>, master_password_hash: Option<String>,
auth_request_id: Option<AuthRequestId>,
auth_request_access_code: Option<String>,
} }
/// User is trying to login and wants to use email 2FA. /// User is trying to login and wants to use email 2FA.
/// Does not require Bearer token /// Does not require Bearer token
#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult #[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
async fn send_email_login(data: Json<SendEmailLoginData>, conn: DbConn) -> EmptyResult { async fn send_email_login(data: Json<SendEmailLoginData>, client_headers: ClientHeaders, conn: DbConn) -> EmptyResult {
let data: SendEmailLoginData = data.into_inner(); let data: SendEmailLoginData = data.into_inner();
if !CONFIG._enable_email_2fa() { if !CONFIG._enable_email_2fa() {
@@ -47,18 +49,41 @@ async fn send_email_login(data: Json<SendEmailLoginData>, conn: DbConn) -> Empty
Some(email) if !email.is_empty() => Some(email), Some(email) if !email.is_empty() => Some(email),
_ => None, _ => None,
}; };
let user = if let Some(email) = email { let master_password_hash = match &data.master_password_hash {
let Some(master_password_hash) = &data.master_password_hash else { Some(password_hash) if !password_hash.is_empty() => Some(password_hash),
err!("No password hash has been submitted.") _ => None,
}; };
let auth_request_id = match &data.auth_request_id {
Some(auth_request_id) if !auth_request_id.is_empty() => Some(auth_request_id),
_ => None,
};
let user = if let Some(email) = email {
let Some(user) = User::find_by_mail(email, &conn).await else { let Some(user) = User::find_by_mail(email, &conn).await else {
err!("Username or password is incorrect. Try again.") err!("Username or password is incorrect. Try again.")
}; };
// Check password if let Some(master_password_hash) = master_password_hash {
if !user.check_valid_password(master_password_hash) { // Check password
err!("Username or password is incorrect. Try again.") if !user.check_valid_password(master_password_hash) {
err!("Username or password is incorrect. Try again.")
}
} else if let Some(auth_request_id) = auth_request_id {
let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_id, &conn).await else {
err!("AuthRequest doesn't exist", "User not found")
};
let Some(code) = &data.auth_request_access_code else {
err!("no auth request access code")
};
if auth_request.device_type != client_headers.device_type
|| auth_request.request_ip != client_headers.ip.ip.to_string()
|| !auth_request.check_access_code(code)
{
err!("AuthRequest doesn't exist", "Invalid device, IP or code")
}
} else {
err!("No password hash has been submitted.")
} }
user user

View File

@@ -144,7 +144,7 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
let (mut challenge, state) = WEBAUTHN.start_passkey_registration( let (mut challenge, state) = WEBAUTHN.start_passkey_registration(
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
&user.email, &user.email,
&user.name, user.display_name(),
Some(registrations), Some(registrations),
)?; )?;

View File

@@ -266,7 +266,7 @@ async fn _sso_login(
Some((user, _)) if !user.enabled => { Some((user, _)) if !user.enabled => {
err!( err!(
"This user has been disabled", "This user has been disabled",
format!("IP: {}. Username: {}.", ip.ip, user.name), format!("IP: {}. Username: {}.", ip.ip, user.display_name()),
ErrorEvent { ErrorEvent {
event: EventType::UserFailedLogIn event: EventType::UserFailedLogIn
} }
@@ -482,14 +482,18 @@ async fn authenticated_response(
Value::Null Value::Null
}; };
let account_keys = json!({ let account_keys = if user.private_key.is_some() {
"publicKeyEncryptionKeyPair": { json!({
"wrappedPrivateKey": user.private_key, "publicKeyEncryptionKeyPair": {
"publicKey": user.public_key, "wrappedPrivateKey": user.private_key,
"Object": "publicKeyEncryptionKeyPair" "publicKey": user.public_key,
}, "Object": "publicKeyEncryptionKeyPair"
"Object": "privateKeys" },
}); "Object": "privateKeys"
})
} else {
Value::Null
};
let mut result = json!({ let mut result = json!({
"access_token": auth_tokens.access_token(), "access_token": auth_tokens.access_token(),
@@ -521,7 +525,7 @@ async fn authenticated_response(
result["TwoFactorToken"] = Value::String(token); result["TwoFactorToken"] = Value::String(token);
} }
info!("User {} logged in successfully. IP: {}", &user.name, ip.ip); info!("User {} logged in successfully. IP: {}", user.display_name(), ip.ip);
Ok(Json(result)) Ok(Json(result))
} }
@@ -610,6 +614,25 @@ async fn _user_api_key_login(
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip); info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
let has_master_password = !user.password_hash.is_empty();
let master_password_unlock = if has_master_password {
json!({
"Kdf": {
"KdfType": user.client_kdf_type,
"Iterations": user.client_kdf_iter,
"Memory": user.client_kdf_memory,
"Parallelism": user.client_kdf_parallelism
},
// This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps.
// https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26
"MasterKeyEncryptedUserKey": user.akey,
"MasterKeyWrappedUserKey": user.akey,
"Salt": user.email
})
} else {
Value::Null
};
// Note: No refresh_token is returned. The CLI just repeats the // Note: No refresh_token is returned. The CLI just repeats the
// client_credentials login flow when the existing token expires. // client_credentials login flow when the existing token expires.
let result = json!({ let result = json!({
@@ -625,6 +648,11 @@ async fn _user_api_key_login(
"KdfParallelism": user.client_kdf_parallelism, "KdfParallelism": user.client_kdf_parallelism,
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
"scope": AuthMethod::UserApiKey.scope(), "scope": AuthMethod::UserApiKey.scope(),
"UserDecryptionOptions": {
"HasMasterPassword": has_master_password,
"MasterPasswordUnlock": master_password_unlock,
"Object": "userDecryptionOptions"
},
}); });
Ok(Json(result)) Ok(Json(result))
@@ -919,6 +947,7 @@ struct RegisterVerificationData {
#[derive(rocket::Responder)] #[derive(rocket::Responder)]
enum RegisterVerificationResponse { enum RegisterVerificationResponse {
#[response(status = 204)]
NoContent(()), NoContent(()),
Token(Json<String>), Token(Json<String>),
} }

View File

@@ -47,6 +47,7 @@ pub type EmptyResult = ApiResult<()>;
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct PasswordOrOtpData { struct PasswordOrOtpData {
#[serde(alias = "MasterPasswordHash")]
master_password_hash: Option<String>, master_password_hash: Option<String>,
otp: Option<String>, otp: Option<String>,
} }

View File

@@ -60,11 +60,12 @@ fn vaultwarden_css() -> Cached<Css<String>> {
"mail_2fa_enabled": CONFIG._enable_email_2fa(), "mail_2fa_enabled": CONFIG._enable_email_2fa(),
"mail_enabled": CONFIG.mail_enabled(), "mail_enabled": CONFIG.mail_enabled(),
"sends_allowed": CONFIG.sends_allowed(), "sends_allowed": CONFIG.sends_allowed(),
"password_hints_allowed": CONFIG.password_hints_allowed(),
"signup_disabled": CONFIG.is_signup_disabled(), "signup_disabled": CONFIG.is_signup_disabled(),
"sso_enabled": CONFIG.sso_enabled(), "sso_enabled": CONFIG.sso_enabled(),
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(), "sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(),
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
"webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(), "webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(),
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
}); });
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) { let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
@@ -238,8 +239,8 @@ pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Erro
"jdenticon-3.3.0.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon-3.3.0.js"))), "jdenticon-3.3.0.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon-3.3.0.js"))),
"datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))), "datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
"datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))), "datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
"jquery-3.7.1.slim.js" => { "jquery-4.0.0.slim.js" => {
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.7.1.slim.js"))) Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-4.0.0.slim.js")))
} }
_ => err!(format!("Static file not found: {filename}")), _ => err!(format!("Static file not found: {filename}")),
} }

View File

@@ -1210,8 +1210,20 @@ pub async fn refresh_tokens(
) -> ApiResult<(Device, AuthTokens)> { ) -> ApiResult<(Device, AuthTokens)> {
let refresh_claims = match decode_refresh(refresh_token) { let refresh_claims = match decode_refresh(refresh_token) {
Err(err) => { Err(err) => {
debug!("Failed to decode {} refresh_token: {refresh_token}", ip.ip); error!("Failed to decode {} refresh_token: {refresh_token}: {err:?}", ip.ip);
err_silent!(format!("Impossible to read refresh_token: {}", err.message())) //err_silent!(format!("Impossible to read refresh_token: {}", err.message()))
// If the token failed to decode, it was probably one of the old style tokens that was just a Base64 string.
// We can generate a claim for them for backwards compatibility. Note that the password refresh claims don't
// check expiration or issuer, so they're not included here.
RefreshJwtClaims {
nbf: 0,
exp: 0,
iss: String::new(),
sub: AuthMethod::Password,
device_token: refresh_token.into(),
token: None,
}
} }
Ok(claims) => claims, Ok(claims) => claims,
}; };

View File

@@ -14,7 +14,7 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
use crate::{ use crate::{
error::Error, error::Error,
util::{get_env, get_env_bool, get_web_vault_version, is_valid_email, parse_experimental_client_feature_flags}, util::{get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags},
}; };
static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| { static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {
@@ -1325,12 +1325,16 @@ fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String {
if embed_images { if embed_images {
"cid:".to_string() "cid:".to_string()
} else { } else {
format!("{domain}/vw_static/") // normalize base_url
let base_url = domain.trim_end_matches('/');
format!("{base_url}/vw_static/")
} }
} }
fn generate_sso_callback_path(domain: &str) -> String { fn generate_sso_callback_path(domain: &str) -> String {
format!("{domain}/identity/connect/oidc-signin") // normalize base_url
let base_url = domain.trim_end_matches('/');
format!("{base_url}/identity/connect/oidc-signin")
} }
/// Generate the correct URL for the icon service. /// Generate the correct URL for the icon service.
@@ -1845,7 +1849,7 @@ fn to_json<'reg, 'rc>(
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then. // Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added. // The default is based upon the version since this feature is added.
static WEB_VAULT_VERSION: LazyLock<semver::Version> = LazyLock::new(|| { static WEB_VAULT_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {
let vault_version = get_web_vault_version(); let vault_version = get_active_web_release();
// Use a single regex capture to extract version components // Use a single regex capture to extract version components
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap(); let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
re.captures(&vault_version) re.captures(&vault_version)

View File

@@ -177,7 +177,9 @@ impl AuthRequest {
} }
pub async fn purge_expired_auth_requests(conn: &DbConn) { pub async fn purge_expired_auth_requests(conn: &DbConn) {
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(5).unwrap(); //after 5 minutes, clients reject the request // delete auth requests older than 15 minutes which is functionally equivalent to upstream:
// https://github.com/bitwarden/server/blob/f8ee2270409f7a13125cd414c450740af605a175/src/Sql/dbo/Auth/Stored%20Procedures/AuthRequest_DeleteIfExpired.sql
let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(15).unwrap();
for auth_request in Self::find_created_before(&expiry_time, conn).await { for auth_request in Self::find_created_before(&expiry_time, conn).await {
auth_request.delete(conn).await.ok(); auth_request.delete(conn).await.ok();
} }

View File

@@ -231,6 +231,15 @@ impl User {
pub fn reset_stamp_exception(&mut self) { pub fn reset_stamp_exception(&mut self) {
self.stamp_exception = None; self.stamp_exception = None;
} }
pub fn display_name(&self) -> &str {
// default to email if name is empty
if !&self.name.is_empty() {
&self.name
} else {
&self.email
}
}
} }
/// Database methods /// Database methods

View File

@@ -126,7 +126,7 @@ fn parse_args() {
exit(0); exit(0);
} else if pargs.contains(["-v", "--version"]) { } else if pargs.contains(["-v", "--version"]) {
config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed); config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed);
let web_vault_version = util::get_web_vault_version(); let web_vault_version = util::get_active_web_release();
println!("Vaultwarden {version}"); println!("Vaultwarden {version}");
println!("Web-Vault {web_vault_version}"); println!("Web-Vault {web_vault_version}");
exit(0); exit(0);

View File

@@ -29,7 +29,7 @@ function isValidIp(ip) {
return ipv4Regex.test(ip) || ipv6Regex.test(ip); return ipv4Regex.test(ip) || ipv6Regex.test(ip);
} }
function checkVersions(platform, installed, latest, commit=null, pre_release=false) { function checkVersions(platform, installed, latest, commit=null, compare_order=0) {
if (installed === "-" || latest === "-") { if (installed === "-" || latest === "-") {
document.getElementById(`${platform}-failed`).classList.remove("d-none"); document.getElementById(`${platform}-failed`).classList.remove("d-none");
return; return;
@@ -37,7 +37,7 @@ function checkVersions(platform, installed, latest, commit=null, pre_release=fal
// Only check basic versions, no commit revisions // Only check basic versions, no commit revisions
if (commit === null || installed.indexOf("-") === -1) { if (commit === null || installed.indexOf("-") === -1) {
if (platform === "web" && pre_release === true) { if (platform === "web" && compare_order === 1) {
document.getElementById(`${platform}-prerelease`).classList.remove("d-none"); document.getElementById(`${platform}-prerelease`).classList.remove("d-none");
} else if (installed == latest) { } else if (installed == latest) {
document.getElementById(`${platform}-success`).classList.remove("d-none"); document.getElementById(`${platform}-success`).classList.remove("d-none");
@@ -83,7 +83,7 @@ async function generateSupportString(event, dj) {
let supportString = "### Your environment (Generated via diagnostics page)\n\n"; let supportString = "### Your environment (Generated via diagnostics page)\n\n";
supportString += `* Vaultwarden version: v${dj.current_release}\n`; supportString += `* Vaultwarden version: v${dj.current_release}\n`;
supportString += `* Web-vault version: v${dj.web_vault_version}\n`; supportString += `* Web-vault version: v${dj.active_web_release}\n`;
supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`; supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`;
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`; supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
supportString += `* Database type: ${dj.db_type}\n`; supportString += `* Database type: ${dj.db_type}\n`;
@@ -208,9 +208,9 @@ function initVersionCheck(dj) {
} }
checkVersions("server", serverInstalled, serverLatest, serverLatestCommit); checkVersions("server", serverInstalled, serverLatest, serverLatestCommit);
const webInstalled = dj.web_vault_version; const webInstalled = dj.active_web_release;
const webLatest = dj.latest_web_build; const webLatest = dj.latest_web_release;
checkVersions("web", webInstalled, webLatest, null, dj.web_vault_pre_release); checkVersions("web", webInstalled, webLatest, null, dj.web_vault_compare);
} }
function checkDns(dns_resolved) { function checkDns(dns_resolved) {

View File

@@ -4,10 +4,10 @@
* *
* To rebuild or modify this file with the latest versions of the included * To rebuild or modify this file with the latest versions of the included
* software please visit: * software please visit:
* https://datatables.net/download/#bs5/dt-2.3.5 * https://datatables.net/download/#bs5/dt-2.3.7
* *
* Included libraries: * Included libraries:
* DataTables 2.3.5 * DataTables 2.3.7
*/ */
:root { :root {
@@ -88,42 +88,42 @@ table.dataTable thead > tr > th:active,
table.dataTable thead > tr > td:active { table.dataTable thead > tr > td:active {
outline: none; outline: none;
} }
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before,
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before,
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before { table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before {
position: absolute; position: absolute;
display: block; display: block;
bottom: 50%; bottom: 50%;
content: "\25B2"; content: "\25B2";
content: "\25B2"/""; content: "\25B2"/"";
} }
table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after,
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after,
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after {
position: absolute; position: absolute;
display: block; display: block;
top: 50%; top: 50%;
content: "\25BC"; content: "\25BC";
content: "\25BC"/""; content: "\25BC"/"";
} }
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order,
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order,
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order,
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order,
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order { table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order {
position: relative; position: relative;
width: 12px; width: 12px;
height: 24px; height: 20px;
} }
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after,
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before,
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:after,
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:before,
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after,
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before,
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:after,
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:before,
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after {
left: 0; left: 0;
opacity: 0.125; opacity: 0.125;
line-height: 9px; line-height: 9px;
@@ -140,15 +140,15 @@ table.dataTable thead > tr > td.dt-orderable-desc:hover {
outline: 2px solid rgba(0, 0, 0, 0.05); outline: 2px solid rgba(0, 0, 0, 0.05);
outline-offset: -2px; outline-offset: -2px;
} }
table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after,
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before,
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after {
opacity: 0.6; opacity: 0.6;
} }
table.dataTable thead > tr > th.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) span.dt-column-order:empty, table.dataTable thead > tr > th.sorting_desc_disabled span.dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty, table.dataTable thead > tr > th.sorting_desc_disabled .dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled .dt-column-order:before,
table.dataTable thead > tr > td.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) span.dt-column-order:empty, table.dataTable thead > tr > td.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty,
table.dataTable thead > tr > td.sorting_desc_disabled span.dt-column-order:after, table.dataTable thead > tr > td.sorting_desc_disabled .dt-column-order:after,
table.dataTable thead > tr > td.sorting_asc_disabled span.dt-column-order:before { table.dataTable thead > tr > td.sorting_asc_disabled .dt-column-order:before {
display: none; display: none;
} }
table.dataTable thead > tr > th:active, table.dataTable thead > tr > th:active,
@@ -169,24 +169,24 @@ table.dataTable tfoot > tr > td div.dt-column-footer {
align-items: var(--dt-header-align-items); align-items: var(--dt-header-align-items);
gap: 4px; gap: 4px;
} }
table.dataTable thead > tr > th div.dt-column-header span.dt-column-title, table.dataTable thead > tr > th div.dt-column-header .dt-column-title,
table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title, table.dataTable thead > tr > th div.dt-column-footer .dt-column-title,
table.dataTable thead > tr > td div.dt-column-header span.dt-column-title, table.dataTable thead > tr > td div.dt-column-header .dt-column-title,
table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title, table.dataTable thead > tr > td div.dt-column-footer .dt-column-title,
table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title, table.dataTable tfoot > tr > th div.dt-column-header .dt-column-title,
table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title, table.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title,
table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title, table.dataTable tfoot > tr > td div.dt-column-header .dt-column-title,
table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title { table.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title {
flex-grow: 1; flex-grow: 1;
} }
table.dataTable thead > tr > th div.dt-column-header span.dt-column-title:empty, table.dataTable thead > tr > th div.dt-column-header .dt-column-title:empty,
table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title:empty, table.dataTable thead > tr > th div.dt-column-footer .dt-column-title:empty,
table.dataTable thead > tr > td div.dt-column-header span.dt-column-title:empty, table.dataTable thead > tr > td div.dt-column-header .dt-column-title:empty,
table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title:empty, table.dataTable thead > tr > td div.dt-column-footer .dt-column-title:empty,
table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title:empty, table.dataTable tfoot > tr > th div.dt-column-header .dt-column-title:empty,
table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title:empty, table.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title:empty,
table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title:empty, table.dataTable tfoot > tr > td div.dt-column-header .dt-column-title:empty,
table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title:empty { table.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title:empty {
display: none; display: none;
} }
@@ -588,16 +588,16 @@ table.dataTable.table-sm > thead > tr td.dt-ordering-asc,
table.dataTable.table-sm > thead > tr td.dt-ordering-desc { table.dataTable.table-sm > thead > tr td.dt-ordering-desc {
padding-right: 0.25rem; padding-right: 0.25rem;
} }
table.dataTable.table-sm > thead > tr th.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc .dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr td.dt-orderable-asc .dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr td.dt-orderable-desc .dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr td.dt-ordering-asc .dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-ordering-desc span.dt-column-order { table.dataTable.table-sm > thead > tr td.dt-ordering-desc .dt-column-order {
right: 0.25rem; right: 0.25rem;
} }
table.dataTable.table-sm > thead > tr th.dt-type-date span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-date .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric .dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-type-date span.dt-column-order, table.dataTable.table-sm > thead > tr td.dt-type-date .dt-column-order,
table.dataTable.table-sm > thead > tr td.dt-type-numeric span.dt-column-order { table.dataTable.table-sm > thead > tr td.dt-type-numeric .dt-column-order {
left: 0.25rem; left: 0.25rem;
} }
@@ -606,7 +606,8 @@ div.dt-scroll-head table.table-bordered {
} }
div.table-responsive > div.dt-container > div.row { div.table-responsive > div.dt-container > div.row {
margin: 0; margin-left: 0;
margin-right: 0;
} }
div.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child { div.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child {
padding-left: 0; padding-left: 0;

View File

@@ -4,13 +4,13 @@
* *
* To rebuild or modify this file with the latest versions of the included * To rebuild or modify this file with the latest versions of the included
* software please visit: * software please visit:
* https://datatables.net/download/#bs5/dt-2.3.5 * https://datatables.net/download/#bs5/dt-2.3.7
* *
* Included libraries: * Included libraries:
* DataTables 2.3.5 * DataTables 2.3.7
*/ */
/*! DataTables 2.3.5 /*! DataTables 2.3.7
* © SpryMedia Ltd - datatables.net/license * © SpryMedia Ltd - datatables.net/license
*/ */
@@ -186,7 +186,7 @@
"sDestroyWidth": $this[0].style.width, "sDestroyWidth": $this[0].style.width,
"sInstance": sId, "sInstance": sId,
"sTableId": sId, "sTableId": sId,
colgroup: $('<colgroup>').prependTo(this), colgroup: $('<colgroup>'),
fastData: function (row, column, type) { fastData: function (row, column, type) {
return _fnGetCellData(oSettings, row, column, type); return _fnGetCellData(oSettings, row, column, type);
} }
@@ -259,6 +259,7 @@
"orderHandler", "orderHandler",
"titleRow", "titleRow",
"typeDetect", "typeDetect",
"columnTitleTag",
[ "iCookieDuration", "iStateDuration" ], // backwards compat [ "iCookieDuration", "iStateDuration" ], // backwards compat
[ "oSearch", "oPreviousSearch" ], [ "oSearch", "oPreviousSearch" ],
[ "aoSearchCols", "aoPreSearchCols" ], [ "aoSearchCols", "aoPreSearchCols" ],
@@ -423,7 +424,7 @@
if ( oSettings.caption ) { if ( oSettings.caption ) {
if ( caption.length === 0 ) { if ( caption.length === 0 ) {
caption = $('<caption/>').appendTo( $this ); caption = $('<caption/>').prependTo( $this );
} }
caption.html( oSettings.caption ); caption.html( oSettings.caption );
@@ -436,6 +437,14 @@
oSettings.captionNode = caption[0]; oSettings.captionNode = caption[0];
} }
// Place the colgroup element in the correct location for the HTML structure
if (caption.length) {
oSettings.colgroup.insertAfter(caption);
}
else {
oSettings.colgroup.prependTo(oSettings.nTable);
}
if ( thead.length === 0 ) { if ( thead.length === 0 ) {
thead = $('<thead/>').appendTo($this); thead = $('<thead/>').appendTo($this);
} }
@@ -516,7 +525,7 @@
* *
* @type string * @type string
*/ */
builder: "bs5/dt-2.3.5", builder: "bs5/dt-2.3.7",
/** /**
* Buttons. For use with the Buttons extension for DataTables. This is * Buttons. For use with the Buttons extension for DataTables. This is
@@ -1292,7 +1301,7 @@
}; };
// Replaceable function in api.util // Replaceable function in api.util
var _stripHtml = function (input) { var _stripHtml = function (input, replacement) {
if (! input || typeof input !== 'string') { if (! input || typeof input !== 'string') {
return input; return input;
} }
@@ -1304,7 +1313,7 @@
var previous; var previous;
input = input.replace(_re_html, ''); // Complete tags input = input.replace(_re_html, replacement || ''); // Complete tags
// Safety for incomplete script tag - use do / while to ensure that // Safety for incomplete script tag - use do / while to ensure that
// we get all instances // we get all instances
@@ -1769,7 +1778,7 @@
} }
}, },
stripHtml: function (mixed) { stripHtml: function (mixed, replacement) {
var type = typeof mixed; var type = typeof mixed;
if (type === 'function') { if (type === 'function') {
@@ -1777,7 +1786,7 @@
return; return;
} }
else if (type === 'string') { else if (type === 'string') {
return _stripHtml(mixed); return _stripHtml(mixed, replacement);
} }
return mixed; return mixed;
}, },
@@ -3379,7 +3388,7 @@
colspan++; colspan++;
} }
var titleSpan = $('span.dt-column-title', cell); var titleSpan = $('.dt-column-title', cell);
structure[row][column] = { structure[row][column] = {
cell: cell, cell: cell,
@@ -4093,8 +4102,8 @@
} }
// Wrap the column title so we can write to it in future // Wrap the column title so we can write to it in future
if ( $('span.dt-column-title', cell).length === 0) { if ( $('.dt-column-title', cell).length === 0) {
$('<span>') $(document.createElement(settings.columnTitleTag))
.addClass('dt-column-title') .addClass('dt-column-title')
.append(cell.childNodes) .append(cell.childNodes)
.appendTo(cell); .appendTo(cell);
@@ -4105,9 +4114,9 @@
isHeader && isHeader &&
jqCell.filter(':not([data-dt-order=disable])').length !== 0 && jqCell.filter(':not([data-dt-order=disable])').length !== 0 &&
jqCell.parent(':not([data-dt-order=disable])').length !== 0 && jqCell.parent(':not([data-dt-order=disable])').length !== 0 &&
$('span.dt-column-order', cell).length === 0 $('.dt-column-order', cell).length === 0
) { ) {
$('<span>') $(document.createElement(settings.columnTitleTag))
.addClass('dt-column-order') .addClass('dt-column-order')
.appendTo(cell); .appendTo(cell);
} }
@@ -4116,7 +4125,7 @@
// layout for those elements // layout for those elements
var headerFooter = isHeader ? 'header' : 'footer'; var headerFooter = isHeader ? 'header' : 'footer';
if ( $('span.dt-column-' + headerFooter, cell).length === 0) { if ( $('div.dt-column-' + headerFooter, cell).length === 0) {
$('<div>') $('<div>')
.addClass('dt-column-' + headerFooter) .addClass('dt-column-' + headerFooter)
.append(cell.childNodes) .append(cell.childNodes)
@@ -4273,6 +4282,10 @@
// Custom Ajax option to submit the parameters as a JSON string // Custom Ajax option to submit the parameters as a JSON string
if (baseAjax.submitAs === 'json' && typeof data === 'object') { if (baseAjax.submitAs === 'json' && typeof data === 'object') {
baseAjax.data = JSON.stringify(data); baseAjax.data = JSON.stringify(data);
if (!baseAjax.contentType) {
baseAjax.contentType = 'application/json; charset=utf-8';
}
} }
if (typeof ajax === 'function') { if (typeof ajax === 'function') {
@@ -5531,7 +5544,7 @@
var autoClass = _ext.type.className[column.sType]; var autoClass = _ext.type.className[column.sType];
var padding = column.sContentPadding || (scrollX ? '-' : ''); var padding = column.sContentPadding || (scrollX ? '-' : '');
var text = longest + padding; var text = longest + padding;
var insert = longest.indexOf('<') === -1 var insert = longest.indexOf('<') === -1 && longest.indexOf('&') === -1
? document.createTextNode(text) ? document.createTextNode(text)
: text : text
@@ -5719,15 +5732,20 @@
.replace(/id=".*?"/g, '') .replace(/id=".*?"/g, '')
.replace(/name=".*?"/g, ''); .replace(/name=".*?"/g, '');
var s = _stripHtml(cellString) // Don't want Javascript at all in these calculation cells.
cellString = cellString.replace(/<script.*?<\/script>/gi, ' ');
var noHtml = _stripHtml(cellString, ' ')
.replace( /&nbsp;/g, ' ' ); .replace( /&nbsp;/g, ' ' );
// The length is calculated on the text only, but we keep the HTML
// in the string so it can be used in the calculation table
collection.push({ collection.push({
str: s, str: cellString,
len: s.length len: noHtml.length
}); });
allStrings.push(s); allStrings.push(noHtml);
} }
// Order and then cut down to the size we need // Order and then cut down to the size we need
@@ -8782,7 +8800,7 @@
// Automatic - find the _last_ unique cell from the top that is not empty (last for // Automatic - find the _last_ unique cell from the top that is not empty (last for
// backwards compatibility) // backwards compatibility)
for (var i=0 ; i<header.length ; i++) { for (var i=0 ; i<header.length ; i++) {
if (header[i][column].unique && $('span.dt-column-title', header[i][column].cell).text()) { if (header[i][column].unique && $('.dt-column-title', header[i][column].cell).text()) {
target = i; target = i;
} }
} }
@@ -8878,6 +8896,10 @@
return null; return null;
} }
if (col.responsiveVisible === false) {
return null;
}
// Selector // Selector
if (match[1]) { if (match[1]) {
return $(nodes[idx]).filter(match[1]).length > 0 ? idx : null; return $(nodes[idx]).filter(match[1]).length > 0 ? idx : null;
@@ -9089,7 +9111,7 @@
title = undefined; title = undefined;
} }
var span = $('span.dt-column-title', this.column(column).header(row)); var span = $('.dt-column-title', this.column(column).header(row));
if (title !== undefined) { if (title !== undefined) {
span.html(title); span.html(title);
@@ -10263,8 +10285,8 @@
// Needed for header and footer, so pulled into its own function // Needed for header and footer, so pulled into its own function
function cleanHeader(node, className) { function cleanHeader(node, className) {
$(node).find('span.dt-column-order').remove(); $(node).find('.dt-column-order').remove();
$(node).find('span.dt-column-title').each(function () { $(node).find('.dt-column-title').each(function () {
var title = $(this).html(); var title = $(this).html();
$(this).parent().parent().append(title); $(this).parent().parent().append(title);
$(this).remove(); $(this).remove();
@@ -10282,7 +10304,7 @@
* @type string * @type string
* @default Version number * @default Version number
*/ */
DataTable.version = "2.3.5"; DataTable.version = "2.3.7";
/** /**
* Private data store, containing all of the settings objects that are * Private data store, containing all of the settings objects that are
@@ -11450,7 +11472,10 @@
iDeferLoading: null, iDeferLoading: null,
/** Event listeners */ /** Event listeners */
on: null on: null,
/** Title wrapper element type */
columnTitleTag: 'span'
}; };
_fnHungarianMap( DataTable.defaults ); _fnHungarianMap( DataTable.defaults );
@@ -12414,7 +12439,10 @@
orderHandler: true, orderHandler: true,
/** Title row indicator */ /** Title row indicator */
titleRow: null titleRow: null,
/** Title wrapper element type */
columnTitleTag: 'span'
}; };
/** /**

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
<dl class="row"> <dl class="row">
<dt class="col-sm-5">Server Installed <dt class="col-sm-5">Server Installed
<span class="badge bg-success d-none abbr-badge" id="server-success" title="Latest version is installed.">Ok</span> <span class="badge bg-success d-none abbr-badge" id="server-success" title="Latest version is installed.">Ok</span>
<span class="badge bg-warning text-dark d-none abbr-badge" id="server-warning" title="There seems to be an update available.">Update</span> <span class="badge bg-warning text-dark d-none abbr-badge" id="server-warning" title="An update is available.">Update</span>
<span class="badge bg-info text-dark d-none abbr-badge" id="server-branch" title="This is a branched version.">Branched</span> <span class="badge bg-info text-dark d-none abbr-badge" id="server-branch" title="This is a branched version.">Branched</span>
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
@@ -23,17 +23,17 @@
{{#if page_data.web_vault_enabled}} {{#if page_data.web_vault_enabled}}
<dt class="col-sm-5">Web Installed <dt class="col-sm-5">Web Installed
<span class="badge bg-success d-none abbr-badge" id="web-success" title="Latest version is installed.">Ok</span> <span class="badge bg-success d-none abbr-badge" id="web-success" title="Latest version is installed.">Ok</span>
<span class="badge bg-warning text-dark d-none abbr-badge" id="web-warning" title="There seems to be an update available.">Update</span> <span class="badge bg-warning text-dark d-none abbr-badge" id="web-warning" title="An update is available.">Update</span>
<span class="badge bg-info text-dark d-none abbr-badge" id="web-prerelease" title="You seem to be using a pre-release version.">Pre-Release</span> <span class="badge bg-info text-dark d-none abbr-badge" id="web-prerelease" title="You are using a pre-release version.">Pre-Release</span>
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span id="web-installed">{{page_data.web_vault_version}}</span> <span id="web-installed">{{page_data.active_web_release}}</span>
</dd> </dd>
<dt class="col-sm-5">Web Latest <dt class="col-sm-5">Web Latest
<span class="badge bg-secondary d-none abbr-badge" id="web-failed" title="Unable to determine latest version.">Unknown</span> <span class="badge bg-secondary d-none abbr-badge" id="web-failed" title="Unable to determine latest version.">Unknown</span>
</dt> </dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span id="web-latest">{{page_data.latest_web_build}}</span> <span id="web-latest">{{page_data.latest_web_release}}</span>
</dd> </dd>
{{/if}} {{/if}}
{{#unless page_data.web_vault_enabled}} {{#unless page_data.web_vault_enabled}}

View File

@@ -59,7 +59,7 @@
</main> </main>
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" /> <link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
<script src="{{urlpath}}/vw_static/jquery-3.7.1.slim.js"></script> <script src="{{urlpath}}/vw_static/jquery-4.0.0.slim.js"></script>
<script src="{{urlpath}}/vw_static/datatables.js"></script> <script src="{{urlpath}}/vw_static/datatables.js"></script>
<script src="{{urlpath}}/vw_static/admin_organizations.js"></script> <script src="{{urlpath}}/vw_static/admin_organizations.js"></script>
<script src="{{urlpath}}/vw_static/jdenticon-3.3.0.js"></script> <script src="{{urlpath}}/vw_static/jdenticon-3.3.0.js"></script>

View File

@@ -153,7 +153,7 @@
</main> </main>
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" /> <link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
<script src="{{urlpath}}/vw_static/jquery-3.7.1.slim.js"></script> <script src="{{urlpath}}/vw_static/jquery-4.0.0.slim.js"></script>
<script src="{{urlpath}}/vw_static/datatables.js"></script> <script src="{{urlpath}}/vw_static/datatables.js"></script>
<script src="{{urlpath}}/vw_static/admin_users.js"></script> <script src="{{urlpath}}/vw_static/admin_users.js"></script>
<script src="{{urlpath}}/vw_static/jdenticon-3.3.0.js"></script> <script src="{{urlpath}}/vw_static/jdenticon-3.3.0.js"></script>

View File

@@ -192,6 +192,19 @@ bit-nav-item[route="sends"] {
@extend %vw-hide; @extend %vw-hide;
} }
{{/unless}} {{/unless}}
{{#unless password_hints_allowed}}
/* Hide password hints if not allowed */
a[routerlink="/hint"],
{{#if (webver "<2025.12.2")}}
app-change-password > form > .form-group:nth-child(5),
auth-input-password > form > bit-form-field:nth-child(4) {
{{else}}
.vw-password-hint {
{{/if}}
@extend %vw-hide;
}
{{/unless}}
/**** End Dynamic Vaultwarden Changes ****/ /**** End Dynamic Vaultwarden Changes ****/
/**** Include a special user stylesheet for custom changes ****/ /**** Include a special user stylesheet for custom changes ****/
{{#if load_user_scss}} {{#if load_user_scss}}

View File

@@ -531,7 +531,7 @@ struct WebVaultVersion {
version: String, version: String,
} }
pub fn get_web_vault_version() -> String { pub fn get_active_web_release() -> String {
let version_files = [ let version_files = [
format!("{}/vw-version.json", CONFIG.web_vault_folder()), format!("{}/vw-version.json", CONFIG.web_vault_folder()),
format!("{}/version.json", CONFIG.web_vault_folder()), format!("{}/version.json", CONFIG.web_vault_folder()),