mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-11 03:05:58 +03:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9e4d372213 | ||
|
d0bf0ab237 | ||
|
e327583aa5 | ||
|
ead2f02cbd | ||
|
c453528dc1 | ||
|
6ae48aa8c2 | ||
|
88643fd9d5 | ||
|
73e0002219 | ||
|
c49ee47de0 | ||
|
14408396bb | ||
|
6cbb724069 | ||
|
a2316ca091 | ||
|
c476e19796 | ||
|
9f393cfd9d | ||
|
450c4d4d97 | ||
|
75e62abed0 | ||
|
97f9eb1320 | ||
|
53cc8a65af |
@@ -82,6 +82,10 @@
|
|||||||
## Defaults to daily (5 minutes after midnight). Set blank to disable this job.
|
## Defaults to daily (5 minutes after midnight). Set blank to disable this job.
|
||||||
# TRASH_PURGE_SCHEDULE="0 5 0 * * *"
|
# TRASH_PURGE_SCHEDULE="0 5 0 * * *"
|
||||||
##
|
##
|
||||||
|
## Cron schedule of the job that checks for incomplete 2FA logins.
|
||||||
|
## Defaults to once every minute. Set blank to disable this job.
|
||||||
|
# INCOMPLETE_2FA_SCHEDULE="30 * * * * *"
|
||||||
|
##
|
||||||
## Cron schedule of the job that sends expiration reminders to emergency access grantors.
|
## Cron schedule of the job that sends expiration reminders to emergency access grantors.
|
||||||
## Defaults to hourly (5 minutes after the hour). Set blank to disable this job.
|
## Defaults to hourly (5 minutes after the hour). Set blank to disable this job.
|
||||||
# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 5 * * * *"
|
# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 5 * * * *"
|
||||||
@@ -220,6 +224,13 @@
|
|||||||
## This setting applies globally, so make sure to inform all users of any changes to this setting.
|
## This setting applies globally, so make sure to inform all users of any changes to this setting.
|
||||||
# TRASH_AUTO_DELETE_DAYS=
|
# TRASH_AUTO_DELETE_DAYS=
|
||||||
|
|
||||||
|
## Number of minutes to wait before a 2FA-enabled login is considered incomplete,
|
||||||
|
## resulting in an email notification. An incomplete 2FA login is one where the correct
|
||||||
|
## master password was provided but the required 2FA step was not completed, which
|
||||||
|
## potentially indicates a master password compromise. Set to 0 to disable this check.
|
||||||
|
## This setting applies globally to all users.
|
||||||
|
# INCOMPLETE_2FA_TIME_LIMIT=3
|
||||||
|
|
||||||
## Controls the PBBKDF password iterations to apply on the server
|
## Controls the PBBKDF password iterations to apply on the server
|
||||||
## The change only applies when the password is changed
|
## The change only applies when the password is changed
|
||||||
# PASSWORD_ITERATIONS=100000
|
# PASSWORD_ITERATIONS=100000
|
||||||
|
502
Cargo.lock
generated
502
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@@ -2,7 +2,7 @@
|
|||||||
name = "vaultwarden"
|
name = "vaultwarden"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.57"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
@@ -34,11 +34,11 @@ rocket = { version = "=0.5.0-dev", features = ["tls"], default-features = false
|
|||||||
rocket_contrib = "=0.5.0-dev"
|
rocket_contrib = "=0.5.0-dev"
|
||||||
|
|
||||||
# HTTP client
|
# HTTP client
|
||||||
reqwest = { version = "0.11.5", features = ["blocking", "json", "gzip", "brotli", "socks", "cookies"] }
|
reqwest = { version = "0.11.7", features = ["blocking", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
|
||||||
|
|
||||||
# Used for custom short lived cookie jar
|
# Used for custom short lived cookie jar
|
||||||
cookie = "0.15.1"
|
cookie = "0.15.1"
|
||||||
cookie_store = "0.15.0"
|
cookie_store = "0.15.1"
|
||||||
bytes = "1.1.0"
|
bytes = "1.1.0"
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ url = "2.2.2"
|
|||||||
multipart = { version = "0.18.0", features = ["server"], default-features = false }
|
multipart = { version = "0.18.0", features = ["server"], default-features = false }
|
||||||
|
|
||||||
# WebSockets library
|
# WebSockets library
|
||||||
ws = { version = "0.11.0", package = "parity-ws" }
|
ws = { version = "0.11.1", package = "parity-ws" }
|
||||||
|
|
||||||
# MessagePack library
|
# MessagePack library
|
||||||
rmpv = "1.0.0"
|
rmpv = "1.0.0"
|
||||||
@@ -56,7 +56,7 @@ chashmap = "2.2.2"
|
|||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.130", features = ["derive"] }
|
serde = { version = "1.0.130", features = ["derive"] }
|
||||||
serde_json = "1.0.68"
|
serde_json = "1.0.72"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
@@ -95,7 +95,7 @@ jsonwebtoken = "7.2.0"
|
|||||||
|
|
||||||
# U2F library
|
# U2F library
|
||||||
u2f = "0.2.0"
|
u2f = "0.2.0"
|
||||||
webauthn-rs = "0.3.0-alpha.12"
|
webauthn-rs = "0.3.0"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false }
|
yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false }
|
||||||
@@ -112,19 +112,19 @@ num-derive = "0.3.3"
|
|||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
tracing = { version = "0.1.29", features = ["log"] } # Needed to have lettre trace logging used when SMTP_DEBUG is enabled.
|
tracing = { version = "0.1.29", features = ["log"] } # Needed to have lettre trace logging used when SMTP_DEBUG is enabled.
|
||||||
lettre = { version = "0.10.0-rc.3", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
|
lettre = { version = "0.10.0-rc.4", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
|
||||||
|
|
||||||
# Template library
|
# Template library
|
||||||
handlebars = { version = "4.1.3", features = ["dir_source"] }
|
handlebars = { version = "4.1.5", features = ["dir_source"] }
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# For favicon extraction from main website
|
||||||
html5ever = "0.25.1"
|
html5ever = "0.25.1"
|
||||||
markup5ever_rcdom = "0.1.0"
|
markup5ever_rcdom = "0.1.0"
|
||||||
regex = { version = "1.5.4", features = ["std", "perf", "unicode-perl"], default-features = false }
|
regex = { version = "1.5.4", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
data-url = "0.1.0"
|
data-url = "0.1.1"
|
||||||
|
|
||||||
# Used by U2F, JWT and Postgres
|
# Used by U2F, JWT and Postgres
|
||||||
openssl = "0.10.36"
|
openssl = "0.10.38"
|
||||||
|
|
||||||
# URL encoding library
|
# URL encoding library
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
@@ -135,19 +135,16 @@ idna = "0.2.3"
|
|||||||
pico-args = "0.4.2"
|
pico-args = "0.4.2"
|
||||||
|
|
||||||
# Logging panics to logfile instead stderr only
|
# Logging panics to logfile instead stderr only
|
||||||
backtrace = "0.3.61"
|
backtrace = "0.3.63"
|
||||||
|
|
||||||
# Macro ident concatenation
|
# Macro ident concatenation
|
||||||
paste = "1.0.5"
|
paste = "1.0.6"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Use newest ring
|
# Use newest ring
|
||||||
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
||||||
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
||||||
|
|
||||||
# For favicon extraction from main website
|
|
||||||
data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = 'eb7330b5296c0d43816d1346211b74182bb4ae37' }
|
|
||||||
|
|
||||||
# The maintainer of the `job_scheduler` crate doesn't seem to have responded
|
# The maintainer of the `job_scheduler` crate doesn't seem to have responded
|
||||||
# to any issues or PRs for almost a year (as of April 2021). This hopefully
|
# to any issues or PRs for almost a year (as of April 2021). This hopefully
|
||||||
# temporary fork updates Cargo.toml to use more up-to-date dependencies.
|
# temporary fork updates Cargo.toml to use more up-to-date dependencies.
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
{% set build_stage_base_image = "rust:1.55-buster" %}
|
{% set build_stage_base_image = "rust:1.55-buster" %}
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
{% if "amd64" in target_file %}
|
{% if "amd64" in target_file %}
|
||||||
{% set build_stage_base_image = "clux/muslrust:nightly-2021-10-06" %}
|
{% set build_stage_base_image = "clux/muslrust:nightly-2021-10-23" %}
|
||||||
{% set runtime_stage_base_image = "alpine:3.14" %}
|
{% set runtime_stage_base_image = "alpine:3.14" %}
|
||||||
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
||||||
{% elif "armv7" in target_file %}
|
{% elif "armv7" in target_file %}
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
{% set vault_version = "2.23.0c" %}
|
{% set vault_version = "2.25.0" %}
|
||||||
{% set vault_image_digest = "sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459" %}
|
{% set vault_image_digest = "sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527" %}
|
||||||
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# Using the digest instead of the tag name provides better security,
|
# Using the digest instead of the tag name provides better security,
|
||||||
# as the digest of an image is immutable, whereas a tag name can later
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.55-buster as build
|
FROM rust:1.55-buster as build
|
||||||
|
@@ -16,18 +16,18 @@
|
|||||||
# - 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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM clux/muslrust:nightly-2021-10-06 as build
|
FROM clux/muslrust:nightly-2021-10-23 as build
|
||||||
|
|
||||||
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
|
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
|
||||||
ARG DB=sqlite,postgresql
|
ARG DB=sqlite,postgresql
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.55-buster as build
|
FROM rust:1.55-buster as build
|
||||||
|
@@ -16,18 +16,18 @@
|
|||||||
# - 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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM clux/muslrust:nightly-2021-10-06 as build
|
FROM clux/muslrust:nightly-2021-10-23 as build
|
||||||
|
|
||||||
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
|
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
|
||||||
ARG DB=sqlite,postgresql
|
ARG DB=sqlite,postgresql
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.55-buster as build
|
FROM rust:1.55-buster as build
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.55-buster as build
|
FROM rust:1.55-buster as build
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.55-buster as build
|
FROM rust:1.55-buster as build
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.55-buster as build
|
FROM rust:1.55-buster as build
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.55-buster as build
|
FROM rust:1.55-buster as build
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM messense/rust-musl-cross:armv7-musleabihf as build
|
FROM messense/rust-musl-cross:armv7-musleabihf as build
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.55-buster as build
|
FROM rust:1.55-buster as build
|
||||||
|
@@ -16,15 +16,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 vaultwarden/web-vault:v2.23.0c
|
# $ docker pull vaultwarden/web-vault:v2.25.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.23.0c
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.25.0
|
||||||
# [vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459]
|
# [vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527
|
||||||
# [vaultwarden/web-vault:v2.23.0c]
|
# [vaultwarden/web-vault:v2.25.0]
|
||||||
#
|
#
|
||||||
FROM vaultwarden/web-vault@sha256:dc94d303def3583af08816e91803f1c42107645612440f474f553f0cb0f97459 as vault
|
FROM vaultwarden/web-vault@sha256:0df389deac9e83c739a1f4ff595f12f493b6c27cb4a22bb8fcaba9dc49b9b527 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM messense/rust-musl-cross:armv7-musleabihf as build
|
FROM messense/rust-musl-cross:armv7-musleabihf as build
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE twofactor_incomplete;
|
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE twofactor_incomplete (
|
||||||
|
user_uuid CHAR(36) NOT NULL REFERENCES users(uuid),
|
||||||
|
device_uuid CHAR(36) NOT NULL,
|
||||||
|
device_name TEXT NOT NULL,
|
||||||
|
login_time DATETIME NOT NULL,
|
||||||
|
ip_address TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (user_uuid, device_uuid)
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE twofactor_incomplete;
|
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE twofactor_incomplete (
|
||||||
|
user_uuid VARCHAR(40) NOT NULL REFERENCES users(uuid),
|
||||||
|
device_uuid VARCHAR(40) NOT NULL,
|
||||||
|
device_name TEXT NOT NULL,
|
||||||
|
login_time TIMESTAMP NOT NULL,
|
||||||
|
ip_address TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (user_uuid, device_uuid)
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE twofactor_incomplete;
|
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE twofactor_incomplete (
|
||||||
|
user_uuid TEXT NOT NULL REFERENCES users(uuid),
|
||||||
|
device_uuid TEXT NOT NULL,
|
||||||
|
device_name TEXT NOT NULL,
|
||||||
|
login_time DATETIME NOT NULL,
|
||||||
|
ip_address TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (user_uuid, device_uuid)
|
||||||
|
);
|
@@ -1 +1 @@
|
|||||||
nightly-2021-10-14
|
nightly-2021-11-05
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{env, time::Duration};
|
use std::env;
|
||||||
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{Cookie, Cookies, SameSite, Status},
|
http::{Cookie, Cookies, SameSite, Status},
|
||||||
@@ -236,7 +236,7 @@ impl AdminTemplateData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/", rank = 1)]
|
#[get("/", rank = 1)]
|
||||||
fn admin_page(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
fn admin_page(_token: AdminToken) -> ApiResult<Html<String>> {
|
||||||
let text = AdminTemplateData::new().render()?;
|
let text = AdminTemplateData::new().render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
@@ -462,13 +462,13 @@ struct GitCommit {
|
|||||||
fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
||||||
let github_api = get_reqwest_client();
|
let github_api = get_reqwest_client();
|
||||||
|
|
||||||
Ok(github_api.get(url).timeout(Duration::from_secs(10)).send()?.error_for_status()?.json::<T>()?)
|
Ok(github_api.get(url).send()?.error_for_status()?.json::<T>()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_http_access() -> bool {
|
fn has_http_access() -> bool {
|
||||||
let http_access = get_reqwest_client();
|
let http_access = get_reqwest_client();
|
||||||
|
|
||||||
match http_access.head("https://github.com/dani-garcia/vaultwarden").timeout(Duration::from_secs(10)).send() {
|
match http_access.head("https://github.com/dani-garcia/vaultwarden").send() {
|
||||||
Ok(r) => r.status().is_success(),
|
Ok(r) => r.status().is_success(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
@@ -494,7 +494,6 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
|
|||||||
|
|
||||||
// Execute some environment checks
|
// Execute some environment checks
|
||||||
let running_within_docker = is_running_in_docker();
|
let running_within_docker = is_running_in_docker();
|
||||||
let docker_base_image = docker_base_image();
|
|
||||||
let has_http_access = has_http_access();
|
let has_http_access = has_http_access();
|
||||||
let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|
let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|
||||||
|| env::var_os("http_proxy").is_some()
|
|| env::var_os("http_proxy").is_some()
|
||||||
@@ -552,7 +551,7 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
|
|||||||
"web_vault_version": web_vault_version.version,
|
"web_vault_version": web_vault_version.version,
|
||||||
"latest_web_build": latest_web_build,
|
"latest_web_build": latest_web_build,
|
||||||
"running_within_docker": running_within_docker,
|
"running_within_docker": running_within_docker,
|
||||||
"docker_base_image": docker_base_image,
|
"docker_base_image": docker_base_image(),
|
||||||
"has_http_access": has_http_access,
|
"has_http_access": has_http_access,
|
||||||
"ip_header_exists": &ip_header.0.is_some(),
|
"ip_header_exists": &ip_header.0.is_some(),
|
||||||
"ip_header_match": ip_header_name == CONFIG.ip_header(),
|
"ip_header_match": ip_header_name == CONFIG.ip_header(),
|
||||||
|
@@ -454,7 +454,7 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/verify-email")]
|
#[post("/accounts/verify-email")]
|
||||||
fn post_verify_email(headers: Headers, _conn: DbConn) -> EmptyResult {
|
fn post_verify_email(headers: Headers) -> EmptyResult {
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
if !CONFIG.mail_enabled() {
|
if !CONFIG.mail_enabled() {
|
||||||
@@ -654,7 +654,7 @@ struct VerifyPasswordData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/verify-password", data = "<data>")]
|
#[post("/accounts/verify-password", data = "<data>")]
|
||||||
fn verify_password(data: JsonUpcase<VerifyPasswordData>, headers: Headers, _conn: DbConn) -> EmptyResult {
|
fn verify_password(data: JsonUpcase<VerifyPasswordData>, headers: Headers) -> EmptyResult {
|
||||||
let data: VerifyPasswordData = data.into_inner().data;
|
let data: VerifyPasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
|
@@ -539,7 +539,6 @@ fn reject_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> J
|
|||||||
};
|
};
|
||||||
|
|
||||||
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
|
emergency_access.status = EmergencyAccessStatus::Confirmed as i32;
|
||||||
emergency_access.key_encrypted = None;
|
|
||||||
emergency_access.save(&conn)?;
|
emergency_access.save(&conn)?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
|
@@ -9,6 +9,7 @@ pub mod two_factor;
|
|||||||
pub use ciphers::purge_trashed_ciphers;
|
pub use ciphers::purge_trashed_ciphers;
|
||||||
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
||||||
pub use sends::purge_sends;
|
pub use sends::purge_sends;
|
||||||
|
pub use two_factor::send_incomplete_2fa_notifications;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut mod_routes =
|
let mut mod_routes =
|
||||||
|
@@ -1294,71 +1294,43 @@ fn put_policy(
|
|||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
#[get("/organizations/<org_id>/tax")]
|
#[get("/organizations/<org_id>/tax")]
|
||||||
fn get_organization_tax(org_id: String, _headers: Headers, _conn: DbConn) -> EmptyResult {
|
fn get_organization_tax(org_id: String, _headers: Headers) -> Json<Value> {
|
||||||
// Prevent a 404 error, which also causes Javascript errors.
|
// Prevent a 404 error, which also causes Javascript errors.
|
||||||
err!("Only allowed when not self hosted.")
|
// Upstream sends "Only allowed when not self hosted." As an error message.
|
||||||
|
// If we do the same it will also output this to the log, which is overkill.
|
||||||
|
// An empty list/data also works fine.
|
||||||
|
Json(_empty_data_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/plans")]
|
#[get("/plans")]
|
||||||
fn get_plans(_headers: Headers, _conn: DbConn) -> Json<Value> {
|
fn get_plans(_headers: Headers) -> Json<Value> {
|
||||||
|
// Respond with a minimal json just enough to allow the creation of an new organization.
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"Data": [
|
"Data": [{
|
||||||
{
|
|
||||||
"Object": "plan",
|
"Object": "plan",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
"Product": 0,
|
"Product": 0,
|
||||||
"Name": "Free",
|
"Name": "Free",
|
||||||
"IsAnnual": false,
|
|
||||||
"NameLocalizationKey": "planNameFree",
|
"NameLocalizationKey": "planNameFree",
|
||||||
"DescriptionLocalizationKey": "planDescFree",
|
"DescriptionLocalizationKey": "planDescFree"
|
||||||
"CanBeUsedByBusiness": false,
|
}],
|
||||||
"BaseSeats": 2,
|
|
||||||
"BaseStorageGb": null,
|
|
||||||
"MaxCollections": 2,
|
|
||||||
"MaxUsers": 2,
|
|
||||||
"HasAdditionalSeatsOption": false,
|
|
||||||
"MaxAdditionalSeats": null,
|
|
||||||
"HasAdditionalStorageOption": false,
|
|
||||||
"MaxAdditionalStorage": null,
|
|
||||||
"HasPremiumAccessOption": false,
|
|
||||||
"TrialPeriodDays": null,
|
|
||||||
"HasSelfHost": false,
|
|
||||||
"HasPolicies": false,
|
|
||||||
"HasGroups": false,
|
|
||||||
"HasDirectory": false,
|
|
||||||
"HasEvents": false,
|
|
||||||
"HasTotp": false,
|
|
||||||
"Has2fa": false,
|
|
||||||
"HasApi": false,
|
|
||||||
"HasSso": false,
|
|
||||||
"UsersGetPremium": false,
|
|
||||||
"UpgradeSortOrder": -1,
|
|
||||||
"DisplaySortOrder": -1,
|
|
||||||
"LegacyYear": null,
|
|
||||||
"Disabled": false,
|
|
||||||
"StripePlanId": null,
|
|
||||||
"StripeSeatPlanId": null,
|
|
||||||
"StripeStoragePlanId": null,
|
|
||||||
"StripePremiumAccessPlanId": null,
|
|
||||||
"BasePrice": 0.0,
|
|
||||||
"SeatPrice": 0.0,
|
|
||||||
"AdditionalStoragePricePerGb": 0.0,
|
|
||||||
"PremiumAccessOptionPrice": 0.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ContinuationToken": null
|
"ContinuationToken": null
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/plans/sales-tax-rates")]
|
#[get("/plans/sales-tax-rates")]
|
||||||
fn get_plans_tax_rates(_headers: Headers, _conn: DbConn) -> Json<Value> {
|
fn get_plans_tax_rates(_headers: Headers) -> Json<Value> {
|
||||||
// Prevent a 404 error, which also causes Javascript errors.
|
// Prevent a 404 error, which also causes Javascript errors.
|
||||||
Json(json!({
|
Json(_empty_data_json())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _empty_data_json() -> Value {
|
||||||
|
json!({
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"Data": [],
|
"Data": [],
|
||||||
"ContinuationToken": null
|
"ContinuationToken": null
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
use chrono::{Duration, Utc};
|
||||||
use data_encoding::BASE32;
|
use data_encoding::BASE32;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
@@ -7,7 +8,7 @@ use crate::{
|
|||||||
api::{JsonResult, JsonUpcase, NumberOrString, PasswordData},
|
api::{JsonResult, JsonUpcase, NumberOrString, PasswordData},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
crypto,
|
crypto,
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn, DbPool},
|
||||||
mail, CONFIG,
|
mail, CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,3 +157,33 @@ fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, c
|
|||||||
fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
disable_twofactor(data, headers, conn)
|
disable_twofactor(data, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_incomplete_2fa_notifications(pool: DbPool) {
|
||||||
|
debug!("Sending notifications for incomplete 2FA logins");
|
||||||
|
|
||||||
|
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn = match pool.get() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
_ => {
|
||||||
|
error!("Failed to get DB connection in send_incomplete_2fa_notifications()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
let time_limit = Duration::minutes(CONFIG.incomplete_2fa_time_limit());
|
||||||
|
let incomplete_logins = TwoFactorIncomplete::find_logins_before(&(now - time_limit), &conn);
|
||||||
|
for login in incomplete_logins {
|
||||||
|
let user = User::find_by_uuid(&login.user_uuid, &conn).expect("User not found");
|
||||||
|
info!(
|
||||||
|
"User {} did not complete a 2FA login within the configured time limit. IP: {}",
|
||||||
|
user.email, login.ip_address
|
||||||
|
);
|
||||||
|
mail::send_incomplete_2fa_login(&user.email, &login.ip_address, &login.login_time, &login.device_name)
|
||||||
|
.expect("Error sending incomplete 2FA email");
|
||||||
|
login.delete(&conn).expect("Error deleting incomplete 2FA record");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use url::Url;
|
||||||
use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn};
|
use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -22,7 +23,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
|
|
||||||
struct WebauthnConfig {
|
struct WebauthnConfig {
|
||||||
url: String,
|
url: String,
|
||||||
origin: String,
|
origin: Url,
|
||||||
rpid: String,
|
rpid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,13 +32,9 @@ impl WebauthnConfig {
|
|||||||
let domain = CONFIG.domain();
|
let domain = CONFIG.domain();
|
||||||
let domain_origin = CONFIG.domain_origin();
|
let domain_origin = CONFIG.domain_origin();
|
||||||
Webauthn::new(Self {
|
Webauthn::new(Self {
|
||||||
rpid: reqwest::Url::parse(&domain)
|
rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(),
|
||||||
.map(|u| u.domain().map(str::to_owned))
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
url: domain,
|
url: domain,
|
||||||
origin: domain_origin,
|
origin: Url::parse(&domain_origin).unwrap(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +44,7 @@ impl webauthn_rs::WebauthnConfig for WebauthnConfig {
|
|||||||
&self.url
|
&self.url
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_origin(&self) -> &str {
|
fn get_origin(&self) -> &Url {
|
||||||
&self.origin
|
&self.origin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
use chrono::Local;
|
use chrono::Utc;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
request::{Form, FormItems, FromForm},
|
request::{Form, FormItems, FromForm},
|
||||||
@@ -102,10 +102,9 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username))
|
err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username))
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Local::now();
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
||||||
let now = now.naive_utc();
|
|
||||||
if user.last_verifying_at.is_none()
|
if user.last_verifying_at.is_none()
|
||||||
|| now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()
|
|| now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()
|
||||||
> CONFIG.signups_verify_resend_time() as i64
|
> CONFIG.signups_verify_resend_time() as i64
|
||||||
@@ -219,6 +218,8 @@ fn twofactor_auth(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TwoFactorIncomplete::mark_incomplete(user_uuid, &device.uuid, &device.name, ip, conn)?;
|
||||||
|
|
||||||
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect();
|
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect();
|
||||||
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
|
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
|
||||||
|
|
||||||
@@ -262,6 +263,8 @@ fn twofactor_auth(
|
|||||||
_ => err!("Invalid two factor provider"),
|
_ => err!("Invalid two factor provider"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn)?;
|
||||||
|
|
||||||
if !CONFIG.disable_2fa_remember() && remember == 1 {
|
if !CONFIG.disable_2fa_remember() && remember == 1 {
|
||||||
Ok(Some(device.refresh_twofactor_remember()))
|
Ok(Some(device.refresh_twofactor_remember()))
|
||||||
} else {
|
} else {
|
||||||
|
@@ -13,6 +13,7 @@ pub use crate::api::{
|
|||||||
core::purge_sends,
|
core::purge_sends,
|
||||||
core::purge_trashed_ciphers,
|
core::purge_trashed_ciphers,
|
||||||
core::routes as core_routes,
|
core::routes as core_routes,
|
||||||
|
core::two_factor::send_incomplete_2fa_notifications,
|
||||||
core::{emergency_notification_reminder_job, emergency_request_timeout_job},
|
core::{emergency_notification_reminder_job, emergency_request_timeout_job},
|
||||||
icons::routes as icons_routes,
|
icons::routes as icons_routes,
|
||||||
identity::routes as identity_routes,
|
identity::routes as identity_routes,
|
||||||
|
@@ -4,7 +4,7 @@ use rocket::Route;
|
|||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use crate::{api::EmptyResult, auth::Headers, db::DbConn, Error, CONFIG};
|
use crate::{api::EmptyResult, auth::Headers, Error, CONFIG};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![negotiate, websockets_err]
|
routes![negotiate, websockets_err]
|
||||||
@@ -30,7 +30,7 @@ fn websockets_err() -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/hub/negotiate")]
|
#[post("/hub/negotiate")]
|
||||||
fn negotiate(_headers: Headers, _conn: DbConn) -> Json<JsonValue> {
|
fn negotiate(_headers: Headers) -> Json<JsonValue> {
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use data_encoding::BASE64URL;
|
use data_encoding::BASE64URL;
|
||||||
|
|
||||||
|
@@ -165,7 +165,6 @@ pub fn generate_invite_claims(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// var token = _dataProtector.Protect($"EmergencyAccessInvite {emergencyAccess.Id} {emergencyAccess.Email} {nowMillis}");
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct EmergencyAccessInviteJwtClaims {
|
pub struct EmergencyAccessInviteJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
|
178
src/config.rs
178
src/config.rs
@@ -2,7 +2,6 @@ use std::process::exit;
|
|||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -23,21 +22,6 @@ pub static CONFIG: Lazy<Config> = Lazy::new(|| {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
static PRIVACY_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[\w]").unwrap());
|
|
||||||
const PRIVACY_CONFIG: &[&str] = &[
|
|
||||||
"allowed_iframe_ancestors",
|
|
||||||
"database_url",
|
|
||||||
"domain_origin",
|
|
||||||
"domain_path",
|
|
||||||
"domain",
|
|
||||||
"helo_name",
|
|
||||||
"org_creation_users",
|
|
||||||
"signups_domains_whitelist",
|
|
||||||
"smtp_from",
|
|
||||||
"smtp_host",
|
|
||||||
"smtp_username",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub type Pass = String;
|
pub type Pass = String;
|
||||||
|
|
||||||
macro_rules! make_config {
|
macro_rules! make_config {
|
||||||
@@ -61,7 +45,7 @@ macro_rules! make_config {
|
|||||||
_overrides: Vec<String>,
|
_overrides: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
#[derive(Clone, Default, Deserialize, Serialize)]
|
||||||
pub struct ConfigBuilder {
|
pub struct ConfigBuilder {
|
||||||
$($(
|
$($(
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -133,19 +117,6 @@ macro_rules! make_config {
|
|||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new builder with all the elements from self,
|
|
||||||
/// except those that are equal in both sides
|
|
||||||
fn _remove(&self, other: &Self) -> Self {
|
|
||||||
let mut builder = ConfigBuilder::default();
|
|
||||||
$($(
|
|
||||||
if &self.$name != &other.$name {
|
|
||||||
builder.$name = self.$name.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
)+)+
|
|
||||||
builder
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build(&self) -> ConfigItems {
|
fn build(&self) -> ConfigItems {
|
||||||
let mut config = ConfigItems::default();
|
let mut config = ConfigItems::default();
|
||||||
let _domain_set = self.domain.is_some();
|
let _domain_set = self.domain.is_some();
|
||||||
@@ -161,12 +132,13 @@ macro_rules! make_config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct ConfigItems { $($(pub $name: make_config!{@type $ty, $none_action}, )+)+ }
|
struct ConfigItems { $($( $name: make_config!{@type $ty, $none_action}, )+)+ }
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl Config {
|
impl Config {
|
||||||
$($(
|
$($(
|
||||||
|
$(#[doc = $doc])+
|
||||||
pub fn $name(&self) -> make_config!{@type $ty, $none_action} {
|
pub fn $name(&self) -> make_config!{@type $ty, $none_action} {
|
||||||
self.inner.read().unwrap().config.$name.clone()
|
self.inner.read().unwrap().config.$name.clone()
|
||||||
}
|
}
|
||||||
@@ -189,38 +161,91 @@ macro_rules! make_config {
|
|||||||
|
|
||||||
fn _get_doc(doc: &str) -> serde_json::Value {
|
fn _get_doc(doc: &str) -> serde_json::Value {
|
||||||
let mut split = doc.split("|>").map(str::trim);
|
let mut split = doc.split("|>").map(str::trim);
|
||||||
json!({
|
|
||||||
"name": split.next(),
|
// We do not use the json!() macro here since that causes a lot of macro recursion.
|
||||||
"description": split.next()
|
// This slows down compile time and it also causes issues with rust-analyzer
|
||||||
|
serde_json::Value::Object({
|
||||||
|
let mut doc_json = serde_json::Map::new();
|
||||||
|
doc_json.insert("name".into(), serde_json::to_value(split.next()).unwrap());
|
||||||
|
doc_json.insert("description".into(), serde_json::to_value(split.next()).unwrap());
|
||||||
|
doc_json
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
json!([ $({
|
// We do not use the json!() macro here since that causes a lot of macro recursion.
|
||||||
"group": stringify!($group),
|
// This slows down compile time and it also causes issues with rust-analyzer
|
||||||
"grouptoggle": stringify!($($group_enabled)?),
|
serde_json::Value::Array(<[_]>::into_vec(Box::new([
|
||||||
"groupdoc": make_config!{ @show $($groupdoc)? },
|
$(
|
||||||
"elements": [
|
serde_json::Value::Object({
|
||||||
$( {
|
let mut group = serde_json::Map::new();
|
||||||
"editable": $editable,
|
group.insert("group".into(), (stringify!($group)).into());
|
||||||
"name": stringify!($name),
|
group.insert("grouptoggle".into(), (stringify!($($group_enabled)?)).into());
|
||||||
"value": cfg.$name,
|
group.insert("groupdoc".into(), (make_config!{ @show $($groupdoc)? }).into());
|
||||||
"default": def.$name,
|
|
||||||
"type": _get_form_type(stringify!($ty)),
|
group.insert("elements".into(), serde_json::Value::Array(<[_]>::into_vec(Box::new([
|
||||||
"doc": _get_doc(concat!($($doc),+)),
|
$(
|
||||||
"overridden": overriden.contains(&stringify!($name).to_uppercase()),
|
serde_json::Value::Object({
|
||||||
}, )+
|
let mut element = serde_json::Map::new();
|
||||||
]}, )+ ])
|
element.insert("editable".into(), ($editable).into());
|
||||||
|
element.insert("name".into(), (stringify!($name)).into());
|
||||||
|
element.insert("value".into(), serde_json::to_value(cfg.$name).unwrap());
|
||||||
|
element.insert("default".into(), serde_json::to_value(def.$name).unwrap());
|
||||||
|
element.insert("type".into(), (_get_form_type(stringify!($ty))).into());
|
||||||
|
element.insert("doc".into(), (_get_doc(concat!($($doc),+))).into());
|
||||||
|
element.insert("overridden".into(), (overriden.contains(&stringify!($name).to_uppercase())).into());
|
||||||
|
element
|
||||||
|
}),
|
||||||
|
)+
|
||||||
|
]))));
|
||||||
|
group
|
||||||
|
}),
|
||||||
|
)+
|
||||||
|
])))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_support_json(&self) -> serde_json::Value {
|
pub fn get_support_json(&self) -> serde_json::Value {
|
||||||
|
// Define which config keys need to be masked.
|
||||||
|
// Pass types will always be masked and no need to put them in the list.
|
||||||
|
// Besides Pass, only String types will be masked via _privacy_mask.
|
||||||
|
const PRIVACY_CONFIG: &[&str] = &[
|
||||||
|
"allowed_iframe_ancestors",
|
||||||
|
"database_url",
|
||||||
|
"domain_origin",
|
||||||
|
"domain_path",
|
||||||
|
"domain",
|
||||||
|
"helo_name",
|
||||||
|
"org_creation_users",
|
||||||
|
"signups_domains_whitelist",
|
||||||
|
"smtp_from",
|
||||||
|
"smtp_host",
|
||||||
|
"smtp_username",
|
||||||
|
];
|
||||||
|
|
||||||
let cfg = {
|
let cfg = {
|
||||||
let inner = &self.inner.read().unwrap();
|
let inner = &self.inner.read().unwrap();
|
||||||
inner.config.clone()
|
inner.config.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
json!({ $($(
|
/// We map over the string and remove all alphanumeric, _ and - characters.
|
||||||
stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action },
|
/// This is the fastest way (within micro-seconds) instead of using a regex (which takes mili-seconds)
|
||||||
)+)+ })
|
fn _privacy_mask(value: &str) -> String {
|
||||||
|
value.chars().map(|c|
|
||||||
|
match c {
|
||||||
|
c if c.is_alphanumeric() => '*',
|
||||||
|
'_' => '*',
|
||||||
|
'-' => '*',
|
||||||
|
_ => c
|
||||||
|
}
|
||||||
|
).collect::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::Value::Object({
|
||||||
|
let mut json = serde_json::Map::new();
|
||||||
|
$($(
|
||||||
|
json.insert(stringify!($name).into(), make_config!{ @supportstr $name, cfg.$name, $ty, $none_action });
|
||||||
|
)+)+;
|
||||||
|
json
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_overrides(&self) -> Vec<String> {
|
pub fn get_overrides(&self) -> Vec<String> {
|
||||||
@@ -228,29 +253,30 @@ macro_rules! make_config {
|
|||||||
let inner = &self.inner.read().unwrap();
|
let inner = &self.inner.read().unwrap();
|
||||||
inner._overrides.clone()
|
inner._overrides.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
overrides
|
overrides
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Support string print
|
// Support string print
|
||||||
( @supportstr $name:ident, $value:expr, Pass, option ) => { $value.as_ref().map(|_| String::from("***")) }; // Optional pass, we map to an Option<String> with "***"
|
( @supportstr $name:ident, $value:expr, Pass, option ) => { serde_json::to_value($value.as_ref().map(|_| String::from("***"))).unwrap() }; // Optional pass, we map to an Option<String> with "***"
|
||||||
( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { String::from("***") }; // Required pass, we return "***"
|
( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { "***".into() }; // Required pass, we return "***"
|
||||||
( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config
|
( @supportstr $name:ident, $value:expr, String, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config
|
||||||
if PRIVACY_CONFIG.contains(&stringify!($name)) {
|
if PRIVACY_CONFIG.contains(&stringify!($name)) {
|
||||||
json!($value.as_ref().map(|x| PRIVACY_REGEX.replace_all(&x.to_string(), "${1}*").to_string()))
|
serde_json::to_value($value.as_ref().map(|x| _privacy_mask(x) )).unwrap()
|
||||||
} else {
|
} else {
|
||||||
json!($value)
|
serde_json::to_value($value).unwrap()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config
|
( @supportstr $name:ident, $value:expr, String, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config
|
||||||
if PRIVACY_CONFIG.contains(&stringify!($name)) {
|
if PRIVACY_CONFIG.contains(&stringify!($name)) {
|
||||||
json!(PRIVACY_REGEX.replace_all(&$value.to_string(), "${1}*").to_string())
|
_privacy_mask(&$value).into()
|
||||||
} else {
|
} else {
|
||||||
json!($value)
|
($value).into()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { serde_json::to_value($value).unwrap() }; // Optional other value, we return as is or convert to string to apply the privacy config
|
||||||
|
( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { ($value).into() }; // Required other value, we return as is or convert to string to apply the privacy config
|
||||||
|
|
||||||
// Group or empty string
|
// Group or empty string
|
||||||
( @show ) => { "" };
|
( @show ) => { "" };
|
||||||
@@ -300,8 +326,6 @@ make_config! {
|
|||||||
data_folder: String, false, def, "data".to_string();
|
data_folder: String, false, def, "data".to_string();
|
||||||
/// Database URL
|
/// Database URL
|
||||||
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
|
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
|
||||||
/// Database connection pool size
|
|
||||||
database_max_conns: u32, false, def, 10;
|
|
||||||
/// Icon cache folder
|
/// Icon cache folder
|
||||||
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
|
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
|
||||||
/// Attachments folder
|
/// Attachments folder
|
||||||
@@ -333,6 +357,9 @@ make_config! {
|
|||||||
/// Trash purge schedule |> Cron schedule of the job that checks for trashed items to delete permanently.
|
/// Trash purge schedule |> Cron schedule of the job that checks for trashed items to delete permanently.
|
||||||
/// Defaults to daily. Set blank to disable this job.
|
/// Defaults to daily. Set blank to disable this job.
|
||||||
trash_purge_schedule: String, false, def, "0 5 0 * * *".to_string();
|
trash_purge_schedule: String, false, def, "0 5 0 * * *".to_string();
|
||||||
|
/// Incomplete 2FA login schedule |> Cron schedule of the job that checks for incomplete 2FA logins.
|
||||||
|
/// Defaults to once every minute. Set blank to disable this job.
|
||||||
|
incomplete_2fa_schedule: String, false, def, "30 * * * * *".to_string();
|
||||||
/// Emergency notification reminder schedule |> Cron schedule of the job that sends expiration reminders to emergency access grantors.
|
/// Emergency notification reminder schedule |> Cron schedule of the job that sends expiration reminders to emergency access grantors.
|
||||||
/// Defaults to hourly. Set blank to disable this job.
|
/// Defaults to hourly. Set blank to disable this job.
|
||||||
emergency_notification_reminder_schedule: String, false, def, "0 5 * * * *".to_string();
|
emergency_notification_reminder_schedule: String, false, def, "0 5 * * * *".to_string();
|
||||||
@@ -372,6 +399,13 @@ make_config! {
|
|||||||
/// sure to inform all users of any changes to this setting.
|
/// sure to inform all users of any changes to this setting.
|
||||||
trash_auto_delete_days: i64, true, option;
|
trash_auto_delete_days: i64, true, option;
|
||||||
|
|
||||||
|
/// Incomplete 2FA time limit |> Number of minutes to wait before a 2FA-enabled login is
|
||||||
|
/// considered incomplete, resulting in an email notification. An incomplete 2FA login is one
|
||||||
|
/// where the correct master password was provided but the required 2FA step was not completed,
|
||||||
|
/// which potentially indicates a master password compromise. Set to 0 to disable this check.
|
||||||
|
/// This setting applies globally to all users.
|
||||||
|
incomplete_2fa_time_limit: i64, true, def, 3;
|
||||||
|
|
||||||
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
|
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
|
||||||
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
||||||
/// otherwise it will delete them and they won't be downloaded again.
|
/// otherwise it will delete them and they won't be downloaded again.
|
||||||
@@ -461,6 +495,9 @@ make_config! {
|
|||||||
/// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely
|
/// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely
|
||||||
db_connection_retries: u32, false, def, 15;
|
db_connection_retries: u32, false, def, 15;
|
||||||
|
|
||||||
|
/// Database connection pool size
|
||||||
|
database_max_conns: u32, false, def, 10;
|
||||||
|
|
||||||
/// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
/// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
||||||
disable_admin_token: bool, true, def, false;
|
disable_admin_token: bool, true, def, false;
|
||||||
|
|
||||||
@@ -615,7 +652,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
|
|
||||||
// Check if the icon blacklist regex is valid
|
// Check if the icon blacklist regex is valid
|
||||||
if let Some(ref r) = cfg.icon_blacklist_regex {
|
if let Some(ref r) = cfg.icon_blacklist_regex {
|
||||||
let validate_regex = Regex::new(r);
|
let validate_regex = regex::Regex::new(r);
|
||||||
match validate_regex {
|
match validate_regex {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {:#?}", e)),
|
Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {:#?}", e)),
|
||||||
@@ -707,7 +744,7 @@ impl Config {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
|
fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
|
||||||
let builder = {
|
let builder = {
|
||||||
let usr = &self.inner.read().unwrap()._usr;
|
let usr = &self.inner.read().unwrap()._usr;
|
||||||
let mut _overrides = Vec::new();
|
let mut _overrides = Vec::new();
|
||||||
@@ -861,8 +898,6 @@ where
|
|||||||
|
|
||||||
reg!("email/change_email", ".html");
|
reg!("email/change_email", ".html");
|
||||||
reg!("email/delete_account", ".html");
|
reg!("email/delete_account", ".html");
|
||||||
reg!("email/invite_accepted", ".html");
|
|
||||||
reg!("email/invite_confirmed", ".html");
|
|
||||||
reg!("email/emergency_access_invite_accepted", ".html");
|
reg!("email/emergency_access_invite_accepted", ".html");
|
||||||
reg!("email/emergency_access_invite_confirmed", ".html");
|
reg!("email/emergency_access_invite_confirmed", ".html");
|
||||||
reg!("email/emergency_access_recovery_approved", ".html");
|
reg!("email/emergency_access_recovery_approved", ".html");
|
||||||
@@ -870,6 +905,9 @@ where
|
|||||||
reg!("email/emergency_access_recovery_rejected", ".html");
|
reg!("email/emergency_access_recovery_rejected", ".html");
|
||||||
reg!("email/emergency_access_recovery_reminder", ".html");
|
reg!("email/emergency_access_recovery_reminder", ".html");
|
||||||
reg!("email/emergency_access_recovery_timed_out", ".html");
|
reg!("email/emergency_access_recovery_timed_out", ".html");
|
||||||
|
reg!("email/incomplete_2fa_login", ".html");
|
||||||
|
reg!("email/invite_accepted", ".html");
|
||||||
|
reg!("email/invite_confirmed", ".html");
|
||||||
reg!("email/new_device_logged_in", ".html");
|
reg!("email/new_device_logged_in", ".html");
|
||||||
reg!("email/pw_hint_none", ".html");
|
reg!("email/pw_hint_none", ".html");
|
||||||
reg!("email/pw_hint_some", ".html");
|
reg!("email/pw_hint_some", ".html");
|
||||||
|
@@ -343,36 +343,39 @@ impl Cipher {
|
|||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
// Check whether this cipher is in any collections accessible to the
|
// Check whether this cipher is in any collections accessible to the
|
||||||
// user. If so, retrieve the access flags for each collection.
|
// user. If so, retrieve the access flags for each collection.
|
||||||
let query = ciphers::table
|
let rows = ciphers::table
|
||||||
.filter(ciphers::uuid.eq(&self.uuid))
|
.filter(ciphers::uuid.eq(&self.uuid))
|
||||||
.inner_join(ciphers_collections::table.on(
|
.inner_join(ciphers_collections::table.on(
|
||||||
ciphers::uuid.eq(ciphers_collections::cipher_uuid)))
|
ciphers::uuid.eq(ciphers_collections::cipher_uuid)))
|
||||||
.inner_join(users_collections::table.on(
|
.inner_join(users_collections::table.on(
|
||||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||||
.and(users_collections::user_uuid.eq(user_uuid))))
|
.and(users_collections::user_uuid.eq(user_uuid))))
|
||||||
.select((users_collections::read_only, users_collections::hide_passwords));
|
.select((users_collections::read_only, users_collections::hide_passwords))
|
||||||
|
.load::<(bool, bool)>(conn)
|
||||||
|
.expect("Error getting access restrictions");
|
||||||
|
|
||||||
// There's an edge case where a cipher can be in multiple collections
|
if rows.is_empty() {
|
||||||
// with inconsistent access flags. For example, a cipher could be in
|
|
||||||
// one collection where the user has read-only access, but also in
|
|
||||||
// another collection where the user has read/write access. To handle
|
|
||||||
// this, we do a boolean OR of all values in each of the `read_only`
|
|
||||||
// and `hide_passwords` columns. This could ideally be done as part
|
|
||||||
// of the query, but Diesel doesn't support a max() or bool_or()
|
|
||||||
// function on booleans and this behavior isn't portable anyway.
|
|
||||||
if let Ok(vec) = query.load::<(bool, bool)>(conn) {
|
|
||||||
let mut read_only = false;
|
|
||||||
let mut hide_passwords = false;
|
|
||||||
for (ro, hp) in vec.iter() {
|
|
||||||
read_only |= ro;
|
|
||||||
hide_passwords |= hp;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((read_only, hide_passwords))
|
|
||||||
} else {
|
|
||||||
// This cipher isn't in any collections accessible to the user.
|
// This cipher isn't in any collections accessible to the user.
|
||||||
None
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A cipher can be in multiple collections with inconsistent access flags.
|
||||||
|
// For example, a cipher could be in one collection where the user has
|
||||||
|
// read-only access, but also in another collection where the user has
|
||||||
|
// read/write access. For a flag to be in effect for a cipher, upstream
|
||||||
|
// requires all collections the cipher is in to have that flag set.
|
||||||
|
// Therefore, we do a boolean AND of all values in each of the `read_only`
|
||||||
|
// and `hide_passwords` columns. This could ideally be done as part of the
|
||||||
|
// query, but Diesel doesn't support a min() or bool_and() function on
|
||||||
|
// booleans and this behavior isn't portable anyway.
|
||||||
|
let mut read_only = true;
|
||||||
|
let mut hide_passwords = true;
|
||||||
|
for (ro, hp) in rows.iter() {
|
||||||
|
read_only &= ro;
|
||||||
|
hide_passwords &= hp;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((read_only, hide_passwords))
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,8 +17,7 @@ db_object! {
|
|||||||
pub user_uuid: String,
|
pub user_uuid: String,
|
||||||
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
// https://github.com/bitwarden/core/tree/master/src/Core/Enums
|
pub atype: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
|
||||||
pub atype: i32,
|
|
||||||
pub push_token: Option<String>,
|
pub push_token: Option<String>,
|
||||||
|
|
||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
|
@@ -9,6 +9,7 @@ mod org_policy;
|
|||||||
mod organization;
|
mod organization;
|
||||||
mod send;
|
mod send;
|
||||||
mod two_factor;
|
mod two_factor;
|
||||||
|
mod two_factor_incomplete;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
pub use self::attachment::Attachment;
|
pub use self::attachment::Attachment;
|
||||||
@@ -22,4 +23,5 @@ pub use self::org_policy::{OrgPolicy, OrgPolicyType};
|
|||||||
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
||||||
pub use self::send::{Send, SendType};
|
pub use self::send::{Send, SendType};
|
||||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||||
|
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
||||||
pub use self::user::{Invitation, User, UserStampException};
|
pub use self::user::{Invitation, User, UserStampException};
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
|
||||||
use crate::db::DbConn;
|
|
||||||
use crate::error::MapResult;
|
|
||||||
|
|
||||||
use super::User;
|
use super::User;
|
||||||
|
|
||||||
@@ -161,7 +159,6 @@ impl TwoFactor {
|
|||||||
|
|
||||||
use crate::api::core::two_factor::u2f::U2FRegistration;
|
use crate::api::core::two_factor::u2f::U2FRegistration;
|
||||||
use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
|
use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
|
||||||
use std::convert::TryInto;
|
|
||||||
use webauthn_rs::proto::*;
|
use webauthn_rs::proto::*;
|
||||||
|
|
||||||
for mut u2f in u2f_factors {
|
for mut u2f in u2f_factors {
|
||||||
|
108
src/db/models/two_factor_incomplete.rs
Normal file
108
src/db/models/two_factor_incomplete.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
|
||||||
|
use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG};
|
||||||
|
|
||||||
|
use super::User;
|
||||||
|
|
||||||
|
db_object! {
|
||||||
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
|
#[table_name = "twofactor_incomplete"]
|
||||||
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
|
#[primary_key(user_uuid, device_uuid)]
|
||||||
|
pub struct TwoFactorIncomplete {
|
||||||
|
pub user_uuid: String,
|
||||||
|
// This device UUID is simply what's claimed by the device. It doesn't
|
||||||
|
// necessarily correspond to any UUID in the devices table, since a device
|
||||||
|
// must complete 2FA login before being added into the devices table.
|
||||||
|
pub device_uuid: String,
|
||||||
|
pub device_name: String,
|
||||||
|
pub login_time: NaiveDateTime,
|
||||||
|
pub ip_address: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TwoFactorIncomplete {
|
||||||
|
pub fn mark_incomplete(
|
||||||
|
user_uuid: &str,
|
||||||
|
device_uuid: &str,
|
||||||
|
device_name: &str,
|
||||||
|
ip: &ClientIp,
|
||||||
|
conn: &DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
|
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't update the data for an existing user/device pair, since that
|
||||||
|
// would allow an attacker to arbitrarily delay notifications by
|
||||||
|
// sending repeated 2FA attempts to reset the timer.
|
||||||
|
let existing = Self::find_by_user_and_device(user_uuid, device_uuid, conn);
|
||||||
|
if existing.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::insert_into(twofactor_incomplete::table)
|
||||||
|
.values((
|
||||||
|
twofactor_incomplete::user_uuid.eq(user_uuid),
|
||||||
|
twofactor_incomplete::device_uuid.eq(device_uuid),
|
||||||
|
twofactor_incomplete::device_name.eq(device_name),
|
||||||
|
twofactor_incomplete::login_time.eq(Utc::now().naive_utc()),
|
||||||
|
twofactor_incomplete::ip_address.eq(ip.ip.to_string()),
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error adding twofactor_incomplete record")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_complete(user_uuid: &str, device_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::delete_by_user_and_device(user_uuid, device_uuid, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
twofactor_incomplete::table
|
||||||
|
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
|
||||||
|
.filter(twofactor_incomplete::device_uuid.eq(device_uuid))
|
||||||
|
.first::<TwoFactorIncompleteDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_logins_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
twofactor_incomplete::table
|
||||||
|
.filter(twofactor_incomplete::login_time.lt(dt))
|
||||||
|
.load::<TwoFactorIncompleteDb>(conn)
|
||||||
|
.expect("Error loading twofactor_incomplete")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
|
Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::delete(twofactor_incomplete::table
|
||||||
|
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
|
||||||
|
.filter(twofactor_incomplete::device_uuid.eq(device_uuid)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error in twofactor_incomplete::delete_by_user_and_device()")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error in twofactor_incomplete::delete_all_by_user()")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
@@ -176,7 +176,10 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use super::{Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, UserOrgType, UserOrganization};
|
use super::{
|
||||||
|
Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, TwoFactorIncomplete, UserOrgType,
|
||||||
|
UserOrganization,
|
||||||
|
};
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
@@ -273,6 +276,7 @@ impl User {
|
|||||||
Folder::delete_all_by_user(&self.uuid, conn)?;
|
Folder::delete_all_by_user(&self.uuid, conn)?;
|
||||||
Device::delete_all_by_user(&self.uuid, conn)?;
|
Device::delete_all_by_user(&self.uuid, conn)?;
|
||||||
TwoFactor::delete_all_by_user(&self.uuid, conn)?;
|
TwoFactor::delete_all_by_user(&self.uuid, conn)?;
|
||||||
|
TwoFactorIncomplete::delete_all_by_user(&self.uuid, conn)?;
|
||||||
Invitation::take(&self.email, conn); // Delete invitation if any
|
Invitation::take(&self.email, conn); // Delete invitation if any
|
||||||
|
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
|
@@ -140,6 +140,16 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
twofactor_incomplete (user_uuid, device_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
device_uuid -> Text,
|
||||||
|
device_name -> Text,
|
||||||
|
login_time -> Timestamp,
|
||||||
|
ip_address -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@@ -140,6 +140,16 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
twofactor_incomplete (user_uuid, device_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
device_uuid -> Text,
|
||||||
|
device_name -> Text,
|
||||||
|
login_time -> Timestamp,
|
||||||
|
ip_address -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@@ -140,6 +140,16 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
twofactor_incomplete (user_uuid, device_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
device_uuid -> Text,
|
||||||
|
device_name -> Text,
|
||||||
|
login_time -> Timestamp,
|
||||||
|
ip_address -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@@ -73,7 +73,7 @@ make_error! {
|
|||||||
Serde(SerdeErr): _has_source, _api_error,
|
Serde(SerdeErr): _has_source, _api_error,
|
||||||
JWt(JwtErr): _has_source, _api_error,
|
JWt(JwtErr): _has_source, _api_error,
|
||||||
Handlebars(HbErr): _has_source, _api_error,
|
Handlebars(HbErr): _has_source, _api_error,
|
||||||
//WsError(ws::Error): _has_source, _api_error,
|
|
||||||
Io(IoErr): _has_source, _api_error,
|
Io(IoErr): _has_source, _api_error,
|
||||||
Time(TimeErr): _has_source, _api_error,
|
Time(TimeErr): _has_source, _api_error,
|
||||||
Req(ReqErr): _has_source, _api_error,
|
Req(ReqErr): _has_source, _api_error,
|
||||||
|
35
src/mail.rs
35
src/mail.rs
@@ -1,6 +1,6 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use chrono::{DateTime, Local};
|
use chrono::NaiveDateTime;
|
||||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||||
|
|
||||||
use lettre::{
|
use lettre::{
|
||||||
@@ -394,7 +394,7 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
|||||||
send_email(address, &subject, body_html, body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime<Local>, device: &str) -> EmptyResult {
|
pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult {
|
||||||
use crate::util::upcase_first;
|
use crate::util::upcase_first;
|
||||||
let device = upcase_first(device);
|
let device = upcase_first(device);
|
||||||
|
|
||||||
@@ -405,7 +405,26 @@ pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime<Local>,
|
|||||||
"url": CONFIG.domain(),
|
"url": CONFIG.domain(),
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
"device": device,
|
"device": device,
|
||||||
"datetime": crate::util::format_datetime_local(dt, fmt),
|
"datetime": crate::util::format_naive_datetime_local(dt, fmt),
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_email(address, &subject, body_html, body_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_incomplete_2fa_login(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult {
|
||||||
|
use crate::util::upcase_first;
|
||||||
|
let device = upcase_first(device);
|
||||||
|
|
||||||
|
let fmt = "%A, %B %_d, %Y at %r %Z";
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/incomplete_2fa_login",
|
||||||
|
json!({
|
||||||
|
"url": CONFIG.domain(),
|
||||||
|
"ip": ip,
|
||||||
|
"device": device,
|
||||||
|
"datetime": crate::util::format_naive_datetime_local(dt, fmt),
|
||||||
|
"time_limit": CONFIG.incomplete_2fa_time_limit(),
|
||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -486,10 +505,10 @@ fn send_email(address: &str, subject: &str, body_html: String, body_text: String
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.is_client() {
|
if e.is_client() {
|
||||||
debug!("SMTP Client error: {:#?}", e);
|
debug!("SMTP Client error: {:#?}", e);
|
||||||
err!(format!("SMTP Client error: {}", e.to_string()));
|
err!(format!("SMTP Client error: {}", e));
|
||||||
} else if e.is_transient() {
|
} else if e.is_transient() {
|
||||||
debug!("SMTP 4xx error: {:#?}", e);
|
debug!("SMTP 4xx error: {:#?}", e);
|
||||||
err!(format!("SMTP 4xx error: {}", e.to_string()));
|
err!(format!("SMTP 4xx error: {}", e));
|
||||||
} else if e.is_permanent() {
|
} else if e.is_permanent() {
|
||||||
debug!("SMTP 5xx error: {:#?}", e);
|
debug!("SMTP 5xx error: {:#?}", e);
|
||||||
let mut msg = e.to_string();
|
let mut msg = e.to_string();
|
||||||
@@ -500,13 +519,13 @@ fn send_email(address: &str, subject: &str, body_html: String, body_text: String
|
|||||||
err!(format!("SMTP 5xx error: {}", msg));
|
err!(format!("SMTP 5xx error: {}", msg));
|
||||||
} else if e.is_timeout() {
|
} else if e.is_timeout() {
|
||||||
debug!("SMTP timeout error: {:#?}", e);
|
debug!("SMTP timeout error: {:#?}", e);
|
||||||
err!(format!("SMTP timeout error: {}", e.to_string()));
|
err!(format!("SMTP timeout error: {}", e));
|
||||||
} else if e.is_tls() {
|
} else if e.is_tls() {
|
||||||
debug!("SMTP Encryption error: {:#?}", e);
|
debug!("SMTP Encryption error: {:#?}", e);
|
||||||
err!(format!("SMTP Encryption error: {}", e.to_string()));
|
err!(format!("SMTP Encryption error: {}", e));
|
||||||
} else {
|
} else {
|
||||||
debug!("SMTP {:#?}", e);
|
debug!("SMTP {:#?}", e);
|
||||||
err!(format!("SMTP {}", e.to_string()));
|
err!(format!("SMTP {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
src/main.rs
24
src/main.rs
@@ -1,6 +1,10 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(feature = "unstable", feature(ip))]
|
#![cfg_attr(feature = "unstable", feature(ip))]
|
||||||
#![recursion_limit = "512"]
|
// The recursion_limit is mainly triggered by the json!() macro.
|
||||||
|
// The more key/value pairs there are the more recursion occurs.
|
||||||
|
// We want to keep this as low as possible, but not higher then 128.
|
||||||
|
// If you go above 128 it will cause rust-analyzer to fail,
|
||||||
|
#![recursion_limit = "87"]
|
||||||
|
|
||||||
extern crate openssl;
|
extern crate openssl;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@@ -104,6 +108,14 @@ fn launch_info() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||||
|
// Depending on the main log level we either want to disable or enable logging for trust-dns.
|
||||||
|
// Else if there are timeouts it will clutter the logs since trust-dns uses warn for this.
|
||||||
|
let trust_dns_level = if level >= log::LevelFilter::Debug {
|
||||||
|
level
|
||||||
|
} else {
|
||||||
|
log::LevelFilter::Off
|
||||||
|
};
|
||||||
|
|
||||||
let mut logger = fern::Dispatch::new()
|
let mut logger = fern::Dispatch::new()
|
||||||
.level(level)
|
.level(level)
|
||||||
// Hide unknown certificate errors if using self-signed
|
// Hide unknown certificate errors if using self-signed
|
||||||
@@ -122,6 +134,8 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
|||||||
.level_for("hyper::client", log::LevelFilter::Off)
|
.level_for("hyper::client", log::LevelFilter::Off)
|
||||||
// Prevent cookie_store logs
|
// Prevent cookie_store logs
|
||||||
.level_for("cookie_store", log::LevelFilter::Off)
|
.level_for("cookie_store", log::LevelFilter::Off)
|
||||||
|
// Variable level for trust-dns used by reqwest
|
||||||
|
.level_for("trust_dns_proto", trust_dns_level)
|
||||||
.chain(std::io::stdout());
|
.chain(std::io::stdout());
|
||||||
|
|
||||||
// Enable smtp debug logging only specifically for smtp when need.
|
// Enable smtp debug logging only specifically for smtp when need.
|
||||||
@@ -345,6 +359,14 @@ fn schedule_jobs(pool: db::DbPool) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send email notifications about incomplete 2FA logins, which potentially
|
||||||
|
// indicates that a user's master password has been compromised.
|
||||||
|
if !CONFIG.incomplete_2fa_schedule().is_empty() {
|
||||||
|
sched.add(Job::new(CONFIG.incomplete_2fa_schedule().parse().unwrap(), || {
|
||||||
|
api::send_incomplete_2fa_notifications(pool.clone());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Grant emergency access requests that have met the required wait time.
|
// Grant emergency access requests that have met the required wait time.
|
||||||
// This job should run before the emergency access reminders job to avoid
|
// This job should run before the emergency access reminders job to avoid
|
||||||
// sending reminders for requests that are about to be granted anyway.
|
// sending reminders for requests that are about to be granted anyway.
|
||||||
|
391
src/static/scripts/bootstrap-native.js
vendored
391
src/static/scripts/bootstrap-native.js
vendored
@@ -1,5 +1,5 @@
|
|||||||
/*!
|
/*!
|
||||||
* Native JavaScript for Bootstrap v4.0.6 (https://thednp.github.io/bootstrap.native/)
|
* Native JavaScript for Bootstrap v4.0.8 (https://thednp.github.io/bootstrap.native/)
|
||||||
* Copyright 2015-2021 © dnp_theme
|
* Copyright 2015-2021 © dnp_theme
|
||||||
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
|
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
|
||||||
*/
|
*/
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BSN = factory());
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BSN = factory());
|
||||||
}(this, (function () { 'use strict';
|
})(this, (function () { 'use strict';
|
||||||
|
|
||||||
const transitionEndEvent = 'webkitTransition' in document.head.style ? 'webkitTransitionEnd' : 'transitionend';
|
const transitionEndEvent = 'webkitTransition' in document.head.style ? 'webkitTransitionEnd' : 'transitionend';
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
element.dispatchEvent(closedAlertEvent);
|
element.dispatchEvent(closedAlertEvent);
|
||||||
|
|
||||||
self.dispose();
|
self.dispose();
|
||||||
element.parentNode.removeChild(element);
|
element.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ALERT PRIVATE METHOD
|
// ALERT PRIVATE METHOD
|
||||||
@@ -1022,9 +1022,9 @@
|
|||||||
function isEmptyAnchor(elem) {
|
function isEmptyAnchor(elem) {
|
||||||
const parentAnchor = elem.closest('A');
|
const parentAnchor = elem.closest('A');
|
||||||
// anchor href starts with #
|
// anchor href starts with #
|
||||||
return elem && ((elem.href && elem.href.slice(-1) === '#')
|
return elem && ((elem.hasAttribute('href') && elem.href.slice(-1) === '#')
|
||||||
// OR a child of an anchor with href starts with #
|
// OR a child of an anchor with href starts with #
|
||||||
|| (parentAnchor && parentAnchor.href && parentAnchor.href.slice(-1) === '#'));
|
|| (parentAnchor && parentAnchor.hasAttribute('href') && parentAnchor.href.slice(-1) === '#'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFocus(element) {
|
function setFocus(element) {
|
||||||
@@ -1487,7 +1487,7 @@
|
|||||||
|
|
||||||
function appendOverlay(hasFade, isModal) {
|
function appendOverlay(hasFade, isModal) {
|
||||||
toggleOverlayType(isModal);
|
toggleOverlayType(isModal);
|
||||||
document.body.appendChild(overlay);
|
document.body.append(overlay);
|
||||||
if (hasFade) addClass(overlay, fadeClass);
|
if (hasFade) addClass(overlay, fadeClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1501,12 +1501,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeOverlay() {
|
function removeOverlay() {
|
||||||
const bd = document.body;
|
|
||||||
const currentOpen = getCurrentOpen();
|
const currentOpen = getCurrentOpen();
|
||||||
|
|
||||||
if (!currentOpen) {
|
if (!currentOpen) {
|
||||||
removeClass(overlay, fadeClass);
|
removeClass(overlay, fadeClass);
|
||||||
bd.removeChild(overlay);
|
overlay.remove();
|
||||||
resetScrollbar();
|
resetScrollbar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1928,7 +1927,7 @@
|
|||||||
|
|
||||||
if ((!element.contains(target) && options.backdrop
|
if ((!element.contains(target) && options.backdrop
|
||||||
&& (!trigger || (trigger && !triggers.includes(trigger))))
|
&& (!trigger || (trigger && !triggers.includes(trigger))))
|
||||||
|| offCanvasDismiss.contains(target)) {
|
|| (offCanvasDismiss && offCanvasDismiss.contains(target))) {
|
||||||
self.relatedTarget = target === offCanvasDismiss ? offCanvasDismiss : null;
|
self.relatedTarget = target === offCanvasDismiss ? offCanvasDismiss : null;
|
||||||
self.hide();
|
self.hide();
|
||||||
}
|
}
|
||||||
@@ -2122,19 +2121,6 @@
|
|||||||
.some((mediaType) => element instanceof mediaType);
|
.some((mediaType) => element instanceof mediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closestRelative(element) {
|
|
||||||
let retval = null;
|
|
||||||
let el = element;
|
|
||||||
while (el !== document.body) {
|
|
||||||
el = el.parentElement;
|
|
||||||
if (getComputedStyle(el).position === 'relative') {
|
|
||||||
retval = el;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return retval;
|
|
||||||
}
|
|
||||||
|
|
||||||
// both popovers and tooltips (this, event)
|
// both popovers and tooltips (this, event)
|
||||||
function styleTip(self, e) {
|
function styleTip(self, e) {
|
||||||
const tipClasses = /\b(top|bottom|start|end)+/;
|
const tipClasses = /\b(top|bottom|start|end)+/;
|
||||||
@@ -2148,32 +2134,32 @@
|
|||||||
let tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight };
|
let tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight };
|
||||||
const windowWidth = (document.documentElement.clientWidth || document.body.clientWidth);
|
const windowWidth = (document.documentElement.clientWidth || document.body.clientWidth);
|
||||||
const windowHeight = (document.documentElement.clientHeight || document.body.clientHeight);
|
const windowHeight = (document.documentElement.clientHeight || document.body.clientHeight);
|
||||||
const { element, options, arrow } = self;
|
const {
|
||||||
|
element, options, arrow, positions,
|
||||||
|
} = self;
|
||||||
let { container, placement } = options;
|
let { container, placement } = options;
|
||||||
let parentIsBody = container === document.body;
|
let parentIsBody = container === document.body;
|
||||||
const targetPosition = getComputedStyle(element).position;
|
|
||||||
const parentPosition = getComputedStyle(container).position;
|
const { elementPosition, containerIsStatic, relContainer } = positions;
|
||||||
const staticParent = !parentIsBody && parentPosition === 'static';
|
let { containerIsRelative } = positions;
|
||||||
let relativeParent = !parentIsBody && parentPosition === 'relative';
|
|
||||||
const relContainer = staticParent && closestRelative(container);
|
|
||||||
// static containers should refer to another relative container or the body
|
// static containers should refer to another relative container or the body
|
||||||
container = relContainer || container;
|
container = relContainer || container;
|
||||||
relativeParent = staticParent && relContainer ? 1 : relativeParent;
|
containerIsRelative = containerIsStatic && relContainer ? 1 : containerIsRelative;
|
||||||
parentIsBody = container === document.body;
|
parentIsBody = container === document.body;
|
||||||
const parentRect = container.getBoundingClientRect();
|
const parentRect = container.getBoundingClientRect();
|
||||||
const leftBoundry = relativeParent ? parentRect.left : 0;
|
const leftBoundry = containerIsRelative ? parentRect.left : 0;
|
||||||
const rightBoundry = relativeParent ? parentRect.right : windowWidth;
|
const rightBoundry = containerIsRelative ? parentRect.right : windowWidth;
|
||||||
// this case should not be possible
|
// this case should not be possible
|
||||||
// absoluteParent = !parentIsBody && parentPosition === 'absolute',
|
// containerIsAbsolute = !parentIsBody && containerPosition === 'absolute',
|
||||||
// this case requires a container with placement: relative
|
// this case requires a container with position: relative
|
||||||
const absoluteTarget = targetPosition === 'absolute';
|
const absoluteTarget = elementPosition === 'absolute';
|
||||||
const targetRect = element.getBoundingClientRect();
|
const targetRect = element.getBoundingClientRect();
|
||||||
const scroll = parentIsBody
|
const scroll = parentIsBody
|
||||||
? { x: window.pageXOffset, y: window.pageYOffset }
|
? { x: window.pageXOffset, y: window.pageYOffset }
|
||||||
: { x: container.scrollLeft, y: container.scrollTop };
|
: { x: container.scrollLeft, y: container.scrollTop };
|
||||||
const elemDimensions = { w: element.offsetWidth, h: element.offsetHeight };
|
const elemDimensions = { w: element.offsetWidth, h: element.offsetHeight };
|
||||||
const top = relativeParent ? element.offsetTop : targetRect.top;
|
const top = containerIsRelative ? element.offsetTop : targetRect.top;
|
||||||
const left = relativeParent ? element.offsetLeft : targetRect.left;
|
const left = containerIsRelative ? element.offsetLeft : targetRect.left;
|
||||||
// reset arrow style
|
// reset arrow style
|
||||||
arrow.style.top = '';
|
arrow.style.top = '';
|
||||||
arrow.style.left = '';
|
arrow.style.left = '';
|
||||||
@@ -2245,8 +2231,12 @@
|
|||||||
}
|
}
|
||||||
} else if (['top', 'bottom'].includes(placement)) {
|
} else if (['top', 'bottom'].includes(placement)) {
|
||||||
if (e && isMedia(element)) {
|
if (e && isMedia(element)) {
|
||||||
const eX = !relativeParent ? e.pageX : e.layerX + (absoluteTarget ? element.offsetLeft : 0);
|
const eX = !containerIsRelative
|
||||||
const eY = !relativeParent ? e.pageY : e.layerY + (absoluteTarget ? element.offsetTop : 0);
|
? e.pageX
|
||||||
|
: e.layerX + (absoluteTarget ? element.offsetLeft : 0);
|
||||||
|
const eY = !containerIsRelative
|
||||||
|
? e.pageY
|
||||||
|
: e.layerY + (absoluteTarget ? element.offsetTop : 0);
|
||||||
|
|
||||||
if (placement === 'top') {
|
if (placement === 'top') {
|
||||||
topPosition = eY - tipDimensions.h - (isPopover ? arrowWidth : arrowHeight);
|
topPosition = eY - tipDimensions.h - (isPopover ? arrowWidth : arrowHeight);
|
||||||
@@ -2323,6 +2313,36 @@
|
|||||||
return modal || navbarFixed || document.body;
|
return modal || navbarFixed || document.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closestRelative(element) {
|
||||||
|
let retval = null;
|
||||||
|
let el = element;
|
||||||
|
while (el !== document.body) {
|
||||||
|
el = el.parentElement;
|
||||||
|
if (getComputedStyle(el).position === 'relative') {
|
||||||
|
retval = el;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHtml(element, content, sanitizeFn) {
|
||||||
|
if (typeof content === 'string' && !content.length) return;
|
||||||
|
|
||||||
|
if (typeof content === 'object') {
|
||||||
|
element.append(content);
|
||||||
|
} else {
|
||||||
|
let dirty = content.trim(); // fixing #233
|
||||||
|
|
||||||
|
if (typeof sanitizeFn === 'function') dirty = sanitizeFn(dirty);
|
||||||
|
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
const tempDocument = domParser.parseFromString(dirty, 'text/html');
|
||||||
|
const method = tempDocument.children.length ? 'innerHTML' : 'innerText';
|
||||||
|
element[method] = tempDocument.body[method];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Native JavaScript for Bootstrap 5 | Popover
|
/* Native JavaScript for Bootstrap 5 | Popover
|
||||||
---------------------------------------------- */
|
---------------------------------------------- */
|
||||||
|
|
||||||
@@ -2335,12 +2355,13 @@
|
|||||||
template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>', // string
|
template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>', // string
|
||||||
title: null, // string
|
title: null, // string
|
||||||
content: null, // string
|
content: null, // string
|
||||||
sanitizeFn: null, // function
|
|
||||||
customClass: null, // string
|
customClass: null, // string
|
||||||
dismissible: false, // boolean
|
|
||||||
animation: true, // boolean
|
|
||||||
trigger: 'hover', // string
|
trigger: 'hover', // string
|
||||||
placement: 'top', // string
|
placement: 'top', // string
|
||||||
|
btnClose: '<button class="btn-close" aria-label="Close"></button>', // string
|
||||||
|
sanitizeFn: null, // function
|
||||||
|
dismissible: false, // boolean
|
||||||
|
animation: true, // boolean
|
||||||
delay: 200, // number
|
delay: 200, // number
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2350,11 +2371,8 @@
|
|||||||
const isIphone = navigator.userAgentData
|
const isIphone = navigator.userAgentData
|
||||||
? navigator.userAgentData.brands.some((x) => appleBrands.test(x.brand))
|
? navigator.userAgentData.brands.some((x) => appleBrands.test(x.brand))
|
||||||
: appleBrands.test(navigator.userAgent);
|
: appleBrands.test(navigator.userAgent);
|
||||||
// popoverArrowClass = `${popoverString}-arrow`,
|
|
||||||
const popoverHeaderClass = `${popoverString}-header`;
|
const popoverHeaderClass = `${popoverString}-header`;
|
||||||
const popoverBodyClass = `${popoverString}-body`;
|
const popoverBodyClass = `${popoverString}-body`;
|
||||||
// close btn for dissmissible popover
|
|
||||||
let popoverCloseButton = '<button type="button" class="btn-close"></button>';
|
|
||||||
|
|
||||||
// POPOVER CUSTOM EVENTS
|
// POPOVER CUSTOM EVENTS
|
||||||
// =====================
|
// =====================
|
||||||
@@ -2387,51 +2405,59 @@
|
|||||||
const {
|
const {
|
||||||
animation, customClass, sanitizeFn, placement, dismissible,
|
animation, customClass, sanitizeFn, placement, dismissible,
|
||||||
} = options;
|
} = options;
|
||||||
let { title, content, template } = options;
|
let {
|
||||||
|
title, content,
|
||||||
|
} = options;
|
||||||
|
const {
|
||||||
|
template, btnClose,
|
||||||
|
} = options;
|
||||||
|
|
||||||
// set initial popover class
|
// set initial popover class
|
||||||
const placementClass = `bs-${popoverString}-${tipClassPositions[placement]}`;
|
const placementClass = `bs-${popoverString}-${tipClassPositions[placement]}`;
|
||||||
|
|
||||||
// fixing #233
|
// load template
|
||||||
title = title ? title.trim() : null;
|
let popoverTemplate;
|
||||||
content = content ? content.trim() : null;
|
if (typeof template === 'object') {
|
||||||
|
popoverTemplate = template;
|
||||||
// sanitize title && content
|
} else {
|
||||||
if (sanitizeFn) {
|
const htmlMarkup = document.createElement('div');
|
||||||
title = title ? sanitizeFn(title) : null;
|
setHtml(htmlMarkup, template, sanitizeFn);
|
||||||
content = content ? sanitizeFn(content) : null;
|
popoverTemplate = htmlMarkup.firstChild;
|
||||||
template = template ? sanitizeFn(template) : null;
|
|
||||||
popoverCloseButton = sanitizeFn(popoverCloseButton);
|
|
||||||
}
|
}
|
||||||
|
// set popover markup
|
||||||
|
self.popover = popoverTemplate.cloneNode(true);
|
||||||
|
|
||||||
self.popover = document.createElement('div');
|
|
||||||
const { popover } = self;
|
const { popover } = self;
|
||||||
|
|
||||||
// set id and aria-describedby
|
// set id and role attributes
|
||||||
popover.setAttribute('id', id);
|
popover.setAttribute('id', id);
|
||||||
popover.setAttribute('role', 'tooltip');
|
popover.setAttribute('role', 'tooltip');
|
||||||
|
|
||||||
// load template
|
|
||||||
const popoverTemplate = document.createElement('div');
|
|
||||||
popoverTemplate.innerHTML = template.trim();
|
|
||||||
popover.className = popoverTemplate.firstChild.className;
|
|
||||||
popover.innerHTML = popoverTemplate.firstChild.innerHTML;
|
|
||||||
|
|
||||||
const popoverHeader = queryElement(`.${popoverHeaderClass}`, popover);
|
const popoverHeader = queryElement(`.${popoverHeaderClass}`, popover);
|
||||||
const popoverBody = queryElement(`.${popoverBodyClass}`, popover);
|
const popoverBody = queryElement(`.${popoverBodyClass}`, popover);
|
||||||
|
|
||||||
// set arrow
|
// set arrow and enable access for styleTip
|
||||||
self.arrow = queryElement(`.${popoverString}-arrow`, popover);
|
self.arrow = queryElement(`.${popoverString}-arrow`, popover);
|
||||||
|
|
||||||
// set dismissible button
|
// set dismissible button
|
||||||
if (dismissible) {
|
if (dismissible) {
|
||||||
title = title ? title + popoverCloseButton : title;
|
if (title) {
|
||||||
content = title === null ? +popoverCloseButton : content;
|
if (title instanceof Element) setHtml(title, btnClose, sanitizeFn);
|
||||||
|
else title += btnClose;
|
||||||
|
} else {
|
||||||
|
if (popoverHeader) popoverHeader.remove();
|
||||||
|
if (content instanceof Element) setHtml(content, btnClose, sanitizeFn);
|
||||||
|
else content += btnClose;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill the template with content from data attributes
|
// fill the template with content from options / data attributes
|
||||||
if (title && popoverHeader) popoverHeader.innerHTML = title.trim();
|
// also sanitize title && content
|
||||||
if (content && popoverBody) popoverBody.innerHTML = content.trim();
|
if (title && popoverHeader) setHtml(popoverHeader, title, sanitizeFn);
|
||||||
|
if (content && popoverBody) setHtml(popoverBody, content, sanitizeFn);
|
||||||
|
|
||||||
|
// set btn and enable access for styleTip
|
||||||
|
[self.btn] = popover.getElementsByClassName('btn-close');
|
||||||
|
|
||||||
// set popover animation and placement
|
// set popover animation and placement
|
||||||
if (!hasClass(popover, popoverString)) addClass(popover, popoverString);
|
if (!hasClass(popover, popoverString)) addClass(popover, popoverString);
|
||||||
@@ -2443,9 +2469,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removePopover(self) {
|
function removePopover(self) {
|
||||||
const { element, popover, options } = self;
|
const { element, popover } = self;
|
||||||
element.removeAttribute(ariaDescribedBy);
|
element.removeAttribute(ariaDescribedBy);
|
||||||
options.container.removeChild(popover);
|
popover.remove();
|
||||||
self.timer = null;
|
self.timer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2470,12 +2496,11 @@
|
|||||||
|
|
||||||
function dismissHandlerToggle(self, add) {
|
function dismissHandlerToggle(self, add) {
|
||||||
const action = add ? addEventListener : removeEventListener;
|
const action = add ? addEventListener : removeEventListener;
|
||||||
const { options, element, popover } = self;
|
const { options, element, btn } = self;
|
||||||
const { trigger, dismissible } = options;
|
const { trigger, dismissible } = options;
|
||||||
|
|
||||||
if (dismissible) {
|
if (dismissible) {
|
||||||
const [btnClose] = popover.getElementsByClassName('btn-close');
|
if (btn) btn[action]('click', self.hide);
|
||||||
if (btnClose) btnClose[action]('click', self.hide);
|
|
||||||
} else {
|
} else {
|
||||||
if (trigger === 'focus') element[action]('focusout', self.hide);
|
if (trigger === 'focus') element[action]('focusout', self.hide);
|
||||||
if (trigger === 'hover') document[action]('touchstart', popoverTouchHandler, passiveHandler);
|
if (trigger === 'hover') document[action]('touchstart', popoverTouchHandler, passiveHandler);
|
||||||
@@ -2488,12 +2513,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function popoverShowTrigger(self) {
|
function popoverShowTrigger(self) {
|
||||||
dismissHandlerToggle(self, 1);
|
|
||||||
self.element.dispatchEvent(shownPopoverEvent);
|
self.element.dispatchEvent(shownPopoverEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function popoverHideTrigger(self) {
|
function popoverHideTrigger(self) {
|
||||||
dismissHandlerToggle(self);
|
|
||||||
removePopover(self);
|
removePopover(self);
|
||||||
self.element.dispatchEvent(hiddenPopoverEvent);
|
self.element.dispatchEvent(hiddenPopoverEvent);
|
||||||
}
|
}
|
||||||
@@ -2514,6 +2537,7 @@
|
|||||||
self.timer = null;
|
self.timer = null;
|
||||||
self.popover = null;
|
self.popover = null;
|
||||||
self.arrow = null;
|
self.arrow = null;
|
||||||
|
self.btn = null;
|
||||||
self.enabled = false;
|
self.enabled = false;
|
||||||
// set unique ID for aria-describedby
|
// set unique ID for aria-describedby
|
||||||
self.id = `${popoverString}-${getUID(element)}`;
|
self.id = `${popoverString}-${getUID(element)}`;
|
||||||
@@ -2535,6 +2559,21 @@
|
|||||||
// crate popover
|
// crate popover
|
||||||
createPopover(self);
|
createPopover(self);
|
||||||
|
|
||||||
|
// set positions
|
||||||
|
const { container } = self.options;
|
||||||
|
const elementPosition = getComputedStyle(element).position;
|
||||||
|
const containerPosition = getComputedStyle(container).position;
|
||||||
|
const parentIsBody = container === document.body;
|
||||||
|
const containerIsStatic = !parentIsBody && containerPosition === 'static';
|
||||||
|
const containerIsRelative = !parentIsBody && containerPosition === 'relative';
|
||||||
|
const relContainer = containerIsStatic && closestRelative(container);
|
||||||
|
self.positions = {
|
||||||
|
elementPosition,
|
||||||
|
containerIsRelative,
|
||||||
|
containerIsStatic,
|
||||||
|
relContainer,
|
||||||
|
};
|
||||||
|
|
||||||
// bind
|
// bind
|
||||||
self.update = self.update.bind(self);
|
self.update = self.update.bind(self);
|
||||||
|
|
||||||
@@ -2563,23 +2602,21 @@
|
|||||||
const { container } = options;
|
const { container } = options;
|
||||||
|
|
||||||
clearTimeout(self.timer);
|
clearTimeout(self.timer);
|
||||||
|
if (!isVisibleTip(popover, container)) {
|
||||||
|
element.dispatchEvent(showPopoverEvent);
|
||||||
|
if (showPopoverEvent.defaultPrevented) return;
|
||||||
|
|
||||||
self.timer = setTimeout(() => {
|
// append to the container
|
||||||
if (!isVisibleTip(popover, container)) {
|
container.append(popover);
|
||||||
element.dispatchEvent(showPopoverEvent);
|
element.setAttribute(ariaDescribedBy, id);
|
||||||
if (showPopoverEvent.defaultPrevented) return;
|
|
||||||
|
|
||||||
// append to the container
|
self.update(e);
|
||||||
container.appendChild(popover);
|
if (!hasClass(popover, showClass)) addClass(popover, showClass);
|
||||||
element.setAttribute(ariaDescribedBy, id);
|
dismissHandlerToggle(self, 1);
|
||||||
|
|
||||||
self.update(e);
|
if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self));
|
||||||
if (!hasClass(popover, showClass)) addClass(popover, showClass);
|
else popoverShowTrigger(self);
|
||||||
|
}
|
||||||
if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self));
|
|
||||||
else popoverShowTrigger(self);
|
|
||||||
}
|
|
||||||
}, 17);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hide(e) {
|
hide(e) {
|
||||||
@@ -2596,13 +2633,13 @@
|
|||||||
const { element, popover, options } = self;
|
const { element, popover, options } = self;
|
||||||
|
|
||||||
clearTimeout(self.timer);
|
clearTimeout(self.timer);
|
||||||
|
|
||||||
self.timer = setTimeout(() => {
|
self.timer = setTimeout(() => {
|
||||||
if (isVisibleTip(popover, options.container)) {
|
if (isVisibleTip(popover, options.container)) {
|
||||||
element.dispatchEvent(hidePopoverEvent);
|
element.dispatchEvent(hidePopoverEvent);
|
||||||
if (hidePopoverEvent.defaultPrevented) return;
|
if (hidePopoverEvent.defaultPrevented) return;
|
||||||
|
|
||||||
removeClass(popover, showClass);
|
removeClass(popover, showClass);
|
||||||
|
dismissHandlerToggle(self);
|
||||||
|
|
||||||
if (options.animation) emulateTransitionEnd(popover, () => popoverHideTrigger(self));
|
if (options.animation) emulateTransitionEnd(popover, () => popoverHideTrigger(self));
|
||||||
else popoverHideTrigger(self);
|
else popoverHideTrigger(self);
|
||||||
@@ -2648,7 +2685,7 @@
|
|||||||
const { popover, options } = self;
|
const { popover, options } = self;
|
||||||
const { container, animation } = options;
|
const { container, animation } = options;
|
||||||
if (animation && isVisibleTip(popover, container)) {
|
if (animation && isVisibleTip(popover, container)) {
|
||||||
options.delay = 0; // reset delay
|
self.options.delay = 0; // reset delay
|
||||||
self.hide();
|
self.hide();
|
||||||
emulateTransitionEnd(popover, () => togglePopoverHandlers(self));
|
emulateTransitionEnd(popover, () => togglePopoverHandlers(self));
|
||||||
} else {
|
} else {
|
||||||
@@ -3067,7 +3104,7 @@
|
|||||||
const toastSelector = `.${toastString}`;
|
const toastSelector = `.${toastString}`;
|
||||||
const toastDismissSelector = `[${dataBsDismiss}="${toastString}"]`;
|
const toastDismissSelector = `[${dataBsDismiss}="${toastString}"]`;
|
||||||
const showingClass = 'showing';
|
const showingClass = 'showing';
|
||||||
const hideClass = 'hide';
|
const hideClass = 'hide'; // marked as deprecated
|
||||||
const toastDefaultOptions = {
|
const toastDefaultOptions = {
|
||||||
animation: true,
|
animation: true,
|
||||||
autohide: true,
|
autohide: true,
|
||||||
@@ -3085,10 +3122,7 @@
|
|||||||
// =====================
|
// =====================
|
||||||
function showToastComplete(self) {
|
function showToastComplete(self) {
|
||||||
const { element, options } = self;
|
const { element, options } = self;
|
||||||
if (!options.animation) {
|
removeClass(element, showingClass);
|
||||||
removeClass(element, showingClass);
|
|
||||||
addClass(element, showClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
element.dispatchEvent(shownToastEvent);
|
element.dispatchEvent(shownToastEvent);
|
||||||
if (options.autohide) self.hide();
|
if (options.autohide) self.hide();
|
||||||
@@ -3096,13 +3130,15 @@
|
|||||||
|
|
||||||
function hideToastComplete(self) {
|
function hideToastComplete(self) {
|
||||||
const { element } = self;
|
const { element } = self;
|
||||||
addClass(element, hideClass);
|
removeClass(element, showingClass);
|
||||||
|
removeClass(element, showClass);
|
||||||
|
addClass(element, hideClass); // B/C
|
||||||
element.dispatchEvent(hiddenToastEvent);
|
element.dispatchEvent(hiddenToastEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeToast(self) {
|
function hideToast(self) {
|
||||||
const { element, options } = self;
|
const { element, options } = self;
|
||||||
removeClass(element, showClass);
|
addClass(element, showingClass);
|
||||||
|
|
||||||
if (options.animation) {
|
if (options.animation) {
|
||||||
reflow(element);
|
reflow(element);
|
||||||
@@ -3112,15 +3148,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openToast(self) {
|
function showToast(self) {
|
||||||
const { element, options } = self;
|
const { element, options } = self;
|
||||||
removeClass(element, hideClass);
|
removeClass(element, hideClass); // B/C
|
||||||
|
reflow(element);
|
||||||
|
addClass(element, showClass);
|
||||||
|
addClass(element, showingClass);
|
||||||
|
|
||||||
if (options.animation) {
|
if (options.animation) {
|
||||||
reflow(element);
|
|
||||||
addClass(element, showingClass);
|
|
||||||
addClass(element, showClass);
|
|
||||||
|
|
||||||
emulateTransitionEnd(element, () => showToastComplete(self));
|
emulateTransitionEnd(element, () => showToastComplete(self));
|
||||||
} else {
|
} else {
|
||||||
showToastComplete(self);
|
showToastComplete(self);
|
||||||
@@ -3148,9 +3183,13 @@
|
|||||||
super(toastComponent, target, toastDefaultOptions, config);
|
super(toastComponent, target, toastDefaultOptions, config);
|
||||||
// bind
|
// bind
|
||||||
const self = this;
|
const self = this;
|
||||||
|
const { element, options } = self;
|
||||||
|
|
||||||
|
// set fadeClass, the options.animation will override the markup
|
||||||
|
if (options.animation && !hasClass(element, fadeClass)) addClass(element, fadeClass);
|
||||||
|
else if (!options.animation && hasClass(element, fadeClass)) removeClass(element, fadeClass);
|
||||||
// dismiss button
|
// dismiss button
|
||||||
self.dismiss = queryElement(toastDismissSelector, self.element);
|
self.dismiss = queryElement(toastDismissSelector, element);
|
||||||
|
|
||||||
// bind
|
// bind
|
||||||
self.show = self.show.bind(self);
|
self.show = self.show.bind(self);
|
||||||
@@ -3165,13 +3204,12 @@
|
|||||||
show() {
|
show() {
|
||||||
const self = this;
|
const self = this;
|
||||||
const { element } = self;
|
const { element } = self;
|
||||||
if (element && hasClass(element, hideClass)) {
|
if (element && !hasClass(element, showClass)) {
|
||||||
element.dispatchEvent(showToastEvent);
|
element.dispatchEvent(showToastEvent);
|
||||||
if (showToastEvent.defaultPrevented) return;
|
if (showToastEvent.defaultPrevented) return;
|
||||||
|
|
||||||
addClass(element, fadeClass);
|
|
||||||
clearTimeout(self.timer);
|
clearTimeout(self.timer);
|
||||||
self.timer = setTimeout(() => openToast(self), 10);
|
self.timer = setTimeout(() => showToast(self), 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3184,7 +3222,7 @@
|
|||||||
if (hideToastEvent.defaultPrevented) return;
|
if (hideToastEvent.defaultPrevented) return;
|
||||||
|
|
||||||
clearTimeout(self.timer);
|
clearTimeout(self.timer);
|
||||||
self.timer = setTimeout(() => closeToast(self),
|
self.timer = setTimeout(() => hideToast(self),
|
||||||
noTimer ? 10 : options.delay);
|
noTimer ? 10 : options.delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3192,7 +3230,7 @@
|
|||||||
dispose() {
|
dispose() {
|
||||||
const self = this;
|
const self = this;
|
||||||
const { element, options } = self;
|
const { element, options } = self;
|
||||||
self.hide();
|
self.hide(1);
|
||||||
|
|
||||||
if (options.animation) emulateTransitionEnd(element, () => completeDisposeToast(self));
|
if (options.animation) emulateTransitionEnd(element, () => completeDisposeToast(self));
|
||||||
else completeDisposeToast(self);
|
else completeDisposeToast(self);
|
||||||
@@ -3221,13 +3259,14 @@
|
|||||||
const titleAttr = 'title';
|
const titleAttr = 'title';
|
||||||
const tooltipInnerClass = `${tooltipString}-inner`;
|
const tooltipInnerClass = `${tooltipString}-inner`;
|
||||||
const tooltipDefaultOptions = {
|
const tooltipDefaultOptions = {
|
||||||
title: null,
|
|
||||||
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
|
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
|
||||||
placement: 'top',
|
title: null, // string
|
||||||
animation: true,
|
customClass: null, // string | null
|
||||||
customClass: null,
|
placement: 'top', // string
|
||||||
delay: 200,
|
sanitizeFn: null, // function
|
||||||
sanitizeFn: null,
|
animation: true, // bool
|
||||||
|
html: false, // bool
|
||||||
|
delay: 200, // number
|
||||||
};
|
};
|
||||||
|
|
||||||
// TOOLTIP CUSTOM EVENTS
|
// TOOLTIP CUSTOM EVENTS
|
||||||
@@ -3241,51 +3280,48 @@
|
|||||||
// =======================
|
// =======================
|
||||||
function createTooltip(self) {
|
function createTooltip(self) {
|
||||||
const { options, id } = self;
|
const { options, id } = self;
|
||||||
const placementClass = `bs-${tooltipString}-${tipClassPositions[options.placement]}`;
|
const {
|
||||||
let titleString = options.title.trim();
|
title, template, customClass, animation, placement, sanitizeFn,
|
||||||
|
} = options;
|
||||||
|
const placementClass = `bs-${tooltipString}-${tipClassPositions[placement]}`;
|
||||||
|
|
||||||
// sanitize stuff
|
if (!title) return;
|
||||||
if (options.sanitizeFn) {
|
|
||||||
titleString = options.sanitizeFn(titleString);
|
// load template
|
||||||
options.template = options.sanitizeFn(options.template);
|
let tooltipTemplate;
|
||||||
|
if (typeof template === 'object') {
|
||||||
|
tooltipTemplate = template;
|
||||||
|
} else {
|
||||||
|
const htmlMarkup = document.createElement('div');
|
||||||
|
setHtml(htmlMarkup, template, sanitizeFn);
|
||||||
|
tooltipTemplate = htmlMarkup.firstChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!titleString) return;
|
|
||||||
|
|
||||||
// create tooltip
|
// create tooltip
|
||||||
self.tooltip = document.createElement('div');
|
self.tooltip = tooltipTemplate.cloneNode(true);
|
||||||
const { tooltip } = self;
|
const { tooltip } = self;
|
||||||
|
// set title
|
||||||
// set aria
|
setHtml(queryElement(`.${tooltipInnerClass}`, tooltip), title, sanitizeFn);
|
||||||
|
// set id & role attribute
|
||||||
tooltip.setAttribute('id', id);
|
tooltip.setAttribute('id', id);
|
||||||
|
tooltip.setAttribute('role', tooltipString);
|
||||||
// set markup
|
|
||||||
const tooltipMarkup = document.createElement('div');
|
|
||||||
tooltipMarkup.innerHTML = options.template.trim();
|
|
||||||
|
|
||||||
tooltip.className = tooltipMarkup.firstChild.className;
|
|
||||||
tooltip.innerHTML = tooltipMarkup.firstChild.innerHTML;
|
|
||||||
|
|
||||||
queryElement(`.${tooltipInnerClass}`, tooltip).innerHTML = titleString;
|
|
||||||
|
|
||||||
// set arrow
|
// set arrow
|
||||||
self.arrow = queryElement(`.${tooltipString}-arrow`, tooltip);
|
self.arrow = queryElement(`.${tooltipString}-arrow`, tooltip);
|
||||||
|
|
||||||
// set class and role attribute
|
|
||||||
tooltip.setAttribute('role', tooltipString);
|
|
||||||
// set classes
|
// set classes
|
||||||
if (!hasClass(tooltip, tooltipString)) addClass(tooltip, tooltipString);
|
if (!hasClass(tooltip, tooltipString)) addClass(tooltip, tooltipString);
|
||||||
if (options.animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
|
if (animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
|
||||||
if (options.customClass && !hasClass(tooltip, options.customClass)) {
|
if (customClass && !hasClass(tooltip, customClass)) {
|
||||||
addClass(tooltip, options.customClass);
|
addClass(tooltip, customClass);
|
||||||
}
|
}
|
||||||
if (!hasClass(tooltip, placementClass)) addClass(tooltip, placementClass);
|
if (!hasClass(tooltip, placementClass)) addClass(tooltip, placementClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTooltip(self) {
|
function removeTooltip(self) {
|
||||||
const { element, options, tooltip } = self;
|
const { element, tooltip } = self;
|
||||||
element.removeAttribute(ariaDescribedBy);
|
element.removeAttribute(ariaDescribedBy);
|
||||||
options.container.removeChild(tooltip);
|
tooltip.remove();
|
||||||
self.timer = null;
|
self.timer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3387,6 +3423,21 @@
|
|||||||
self.id = `${tooltipString}-${getUID(element)}`;
|
self.id = `${tooltipString}-${getUID(element)}`;
|
||||||
createTooltip(self);
|
createTooltip(self);
|
||||||
|
|
||||||
|
// set positions
|
||||||
|
const { container } = self.options;
|
||||||
|
const elementPosition = getComputedStyle(element).position;
|
||||||
|
const containerPosition = getComputedStyle(container).position;
|
||||||
|
const parentIsBody = container === document.body;
|
||||||
|
const containerIsStatic = !parentIsBody && containerPosition === 'static';
|
||||||
|
const containerIsRelative = !parentIsBody && containerPosition === 'relative';
|
||||||
|
const relContainer = containerIsStatic && closestRelative(container);
|
||||||
|
self.positions = {
|
||||||
|
elementPosition,
|
||||||
|
containerIsRelative,
|
||||||
|
containerIsStatic,
|
||||||
|
relContainer,
|
||||||
|
};
|
||||||
|
|
||||||
// attach events
|
// attach events
|
||||||
toggleTooltipHandlers(self, 1);
|
toggleTooltipHandlers(self, 1);
|
||||||
}
|
}
|
||||||
@@ -3398,22 +3449,23 @@
|
|||||||
const {
|
const {
|
||||||
options, tooltip, element, id,
|
options, tooltip, element, id,
|
||||||
} = self;
|
} = self;
|
||||||
|
const {
|
||||||
|
container, animation,
|
||||||
|
} = options;
|
||||||
clearTimeout(self.timer);
|
clearTimeout(self.timer);
|
||||||
self.timer = setTimeout(() => {
|
if (!isVisibleTip(tooltip, container)) {
|
||||||
if (!isVisibleTip(tooltip, options.container)) {
|
element.dispatchEvent(showTooltipEvent);
|
||||||
element.dispatchEvent(showTooltipEvent);
|
if (showTooltipEvent.defaultPrevented) return;
|
||||||
if (showTooltipEvent.defaultPrevented) return;
|
|
||||||
|
|
||||||
// append to container
|
// append to container
|
||||||
options.container.appendChild(tooltip);
|
container.append(tooltip);
|
||||||
element.setAttribute(ariaDescribedBy, id);
|
element.setAttribute(ariaDescribedBy, id);
|
||||||
|
|
||||||
self.update(e);
|
self.update(e);
|
||||||
if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
|
if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
|
||||||
if (options.animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
|
if (animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
|
||||||
else tooltipShownAction(self);
|
else tooltipShownAction(self);
|
||||||
}
|
}
|
||||||
}, 20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hide(e) {
|
hide(e) {
|
||||||
@@ -3498,20 +3550,9 @@
|
|||||||
constructor: Tooltip,
|
constructor: Tooltip,
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = "4.0.6";
|
var version = "4.0.8";
|
||||||
|
|
||||||
// import { alertInit } from '../components/alert-native.js';
|
const Version = version;
|
||||||
// import { buttonInit } from '../components/button-native.js';
|
|
||||||
// import { carouselInit } from '../components/carousel-native.js';
|
|
||||||
// import { collapseInit } from '../components/collapse-native.js';
|
|
||||||
// import { dropdownInit } from '../components/dropdown-native.js';
|
|
||||||
// import { modalInit } from '../components/modal-native.js';
|
|
||||||
// import { offcanvasInit } from '../components/offcanvas-native.js';
|
|
||||||
// import { popoverInit } from '../components/popover-native.js';
|
|
||||||
// import { scrollSpyInit } from '../components/scrollspy-native.js';
|
|
||||||
// import { tabInit } from '../components/tab-native.js';
|
|
||||||
// import { toastInit } from '../components/toast-native.js';
|
|
||||||
// import { tooltipInit } from '../components/tooltip-native.js';
|
|
||||||
|
|
||||||
const componentsInit = {
|
const componentsInit = {
|
||||||
Alert: Alert.init,
|
Alert: Alert.init,
|
||||||
@@ -3547,7 +3588,7 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => initCallback(), { once: true });
|
document.addEventListener('DOMContentLoaded', () => initCallback(), { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = {
|
const BSN = {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Carousel,
|
Carousel,
|
||||||
@@ -3562,9 +3603,9 @@
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
|
|
||||||
initCallback,
|
initCallback,
|
||||||
Version: version,
|
Version,
|
||||||
};
|
};
|
||||||
|
|
||||||
return index;
|
return BSN;
|
||||||
|
|
||||||
})));
|
}));
|
937
src/static/scripts/bootstrap.css
vendored
937
src/static/scripts/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
4
src/static/scripts/datatables.css
vendored
4
src/static/scripts/datatables.css
vendored
@@ -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-1.11.2
|
* https://datatables.net/download/#bs5/dt-1.11.3
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 1.11.2
|
* DataTables 1.11.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@charset "UTF-8";
|
@charset "UTF-8";
|
||||||
|
245
src/static/scripts/datatables.js
vendored
245
src/static/scripts/datatables.js
vendored
@@ -4,20 +4,20 @@
|
|||||||
*
|
*
|
||||||
* 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-1.11.2
|
* https://datatables.net/download/#bs5/dt-1.11.3
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 1.11.2
|
* DataTables 1.11.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*! DataTables 1.11.2
|
/*! DataTables 1.11.3
|
||||||
* ©2008-2021 SpryMedia Ltd - datatables.net/license
|
* ©2008-2021 SpryMedia Ltd - datatables.net/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary DataTables
|
* @summary DataTables
|
||||||
* @description Paginate, search and order HTML tables
|
* @description Paginate, search and order HTML tables
|
||||||
* @version 1.11.2
|
* @version 1.11.3
|
||||||
* @file jquery.dataTables.js
|
* @file jquery.dataTables.js
|
||||||
* @author SpryMedia Ltd
|
* @author SpryMedia Ltd
|
||||||
* @contact www.datatables.net
|
* @contact www.datatables.net
|
||||||
@@ -1626,6 +1626,14 @@
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _includes = function (search, start) {
|
||||||
|
if (start === undefined) {
|
||||||
|
start = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.indexOf(search, start) !== -1;
|
||||||
|
};
|
||||||
|
|
||||||
// Array.isArray polyfill.
|
// Array.isArray polyfill.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
|
||||||
if (! Array.isArray) {
|
if (! Array.isArray) {
|
||||||
@@ -1634,6 +1642,10 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! Array.prototype.includes) {
|
||||||
|
Array.prototype.includes = _includes;
|
||||||
|
}
|
||||||
|
|
||||||
// .trim() polyfill
|
// .trim() polyfill
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim
|
||||||
if (!String.prototype.trim) {
|
if (!String.prototype.trim) {
|
||||||
@@ -1642,6 +1654,10 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! String.prototype.includes) {
|
||||||
|
String.prototype.includes = _includes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DataTables utility methods
|
* DataTables utility methods
|
||||||
*
|
*
|
||||||
@@ -2808,9 +2824,18 @@
|
|||||||
return cellData.call( rowData );
|
return cellData.call( rowData );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( cellData === null && type == 'display' ) {
|
if ( cellData === null && type === 'display' ) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( type === 'filter' ) {
|
||||||
|
var fomatters = DataTable.ext.type.search;
|
||||||
|
|
||||||
|
if ( fomatters[ col.sType ] ) {
|
||||||
|
cellData = fomatters[ col.sType ]( cellData );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cellData;
|
return cellData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4565,7 +4590,6 @@
|
|||||||
var columns = settings.aoColumns;
|
var columns = settings.aoColumns;
|
||||||
var column;
|
var column;
|
||||||
var i, j, ien, jen, filterData, cellData, row;
|
var i, j, ien, jen, filterData, cellData, row;
|
||||||
var fomatters = DataTable.ext.type.search;
|
|
||||||
var wasInvalidated = false;
|
var wasInvalidated = false;
|
||||||
|
|
||||||
for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
|
for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
|
||||||
@@ -4580,10 +4604,6 @@
|
|||||||
if ( column.bSearchable ) {
|
if ( column.bSearchable ) {
|
||||||
cellData = _fnGetCellData( settings, i, j, 'filter' );
|
cellData = _fnGetCellData( settings, i, j, 'filter' );
|
||||||
|
|
||||||
if ( fomatters[ column.sType ] ) {
|
|
||||||
cellData = fomatters[ column.sType ]( cellData );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search in DataTables 1.10 is string based. In 1.11 this
|
// Search in DataTables 1.10 is string based. In 1.11 this
|
||||||
// should be altered to also allow strict type checking.
|
// should be altered to also allow strict type checking.
|
||||||
if ( cellData === null ) {
|
if ( cellData === null ) {
|
||||||
@@ -6374,6 +6394,10 @@
|
|||||||
*/
|
*/
|
||||||
function _fnSaveState ( settings )
|
function _fnSaveState ( settings )
|
||||||
{
|
{
|
||||||
|
if (settings._bLoadingState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* Store the interesting variables */
|
/* Store the interesting variables */
|
||||||
var state = {
|
var state = {
|
||||||
time: +new Date(),
|
time: +new Date(),
|
||||||
@@ -6408,99 +6432,129 @@
|
|||||||
*/
|
*/
|
||||||
function _fnLoadState ( settings, oInit, callback )
|
function _fnLoadState ( settings, oInit, callback )
|
||||||
{
|
{
|
||||||
var i, ien;
|
|
||||||
var columns = settings.aoColumns;
|
|
||||||
var loaded = function ( s ) {
|
|
||||||
if ( ! s || ! s.time ) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow custom and plug-in manipulation functions to alter the saved data set and
|
|
||||||
// cancelling of loading by returning false
|
|
||||||
var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );
|
|
||||||
if ( $.inArray( false, abStateLoad ) !== -1 ) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject old data
|
|
||||||
var duration = settings.iStateDuration;
|
|
||||||
if ( duration > 0 && s.time < +new Date() - (duration*1000) ) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Number of columns have changed - all bets are off, no restore of settings
|
|
||||||
if ( s.columns && columns.length !== s.columns.length ) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the saved state so it might be accessed at any time
|
|
||||||
settings.oLoadedState = $.extend( true, {}, s );
|
|
||||||
|
|
||||||
// Restore key features - todo - for 1.11 this needs to be done by
|
|
||||||
// subscribed events
|
|
||||||
if ( s.start !== undefined ) {
|
|
||||||
settings._iDisplayStart = s.start;
|
|
||||||
settings.iInitDisplayStart = s.start;
|
|
||||||
}
|
|
||||||
if ( s.length !== undefined ) {
|
|
||||||
settings._iDisplayLength = s.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order
|
|
||||||
if ( s.order !== undefined ) {
|
|
||||||
settings.aaSorting = [];
|
|
||||||
$.each( s.order, function ( i, col ) {
|
|
||||||
settings.aaSorting.push( col[0] >= columns.length ?
|
|
||||||
[ 0, col[1] ] :
|
|
||||||
col
|
|
||||||
);
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search
|
|
||||||
if ( s.search !== undefined ) {
|
|
||||||
$.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Columns
|
|
||||||
//
|
|
||||||
if ( s.columns ) {
|
|
||||||
for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
|
|
||||||
var col = s.columns[i];
|
|
||||||
|
|
||||||
// Visibility
|
|
||||||
if ( col.visible !== undefined ) {
|
|
||||||
columns[i].bVisible = col.visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search
|
|
||||||
if ( col.search !== undefined ) {
|
|
||||||
$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( ! settings.oFeatures.bStateSave ) {
|
if ( ! settings.oFeatures.bStateSave ) {
|
||||||
callback();
|
callback();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var loaded = function(state) {
|
||||||
|
_fnImplementState(settings, state, callback);
|
||||||
|
}
|
||||||
|
|
||||||
var state = settings.fnStateLoadCallback.call( settings.oInstance, settings, loaded );
|
var state = settings.fnStateLoadCallback.call( settings.oInstance, settings, loaded );
|
||||||
|
|
||||||
if ( state !== undefined ) {
|
if ( state !== undefined ) {
|
||||||
loaded( state );
|
_fnImplementState( settings, state, callback );
|
||||||
}
|
}
|
||||||
// otherwise, wait for the loaded callback to be executed
|
// otherwise, wait for the loaded callback to be executed
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _fnImplementState ( settings, s, callback) {
|
||||||
|
var i, ien;
|
||||||
|
var columns = settings.aoColumns;
|
||||||
|
settings._bLoadingState = true;
|
||||||
|
|
||||||
|
// When StateRestore was introduced the state could now be implemented at any time
|
||||||
|
// Not just initialisation. To do this an api instance is required in some places
|
||||||
|
var api = settings._bInitComplete ? new DataTable.Api(settings) : null;
|
||||||
|
|
||||||
|
if ( ! s || ! s.time ) {
|
||||||
|
settings._bLoadingState = false;
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow custom and plug-in manipulation functions to alter the saved data set and
|
||||||
|
// cancelling of loading by returning false
|
||||||
|
var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );
|
||||||
|
if ( $.inArray( false, abStateLoad ) !== -1 ) {
|
||||||
|
settings._bLoadingState = false;
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject old data
|
||||||
|
var duration = settings.iStateDuration;
|
||||||
|
if ( duration > 0 && s.time < +new Date() - (duration*1000) ) {
|
||||||
|
settings._bLoadingState = false;
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of columns have changed - all bets are off, no restore of settings
|
||||||
|
if ( s.columns && columns.length !== s.columns.length ) {
|
||||||
|
settings._bLoadingState = false;
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the saved state so it might be accessed at any time
|
||||||
|
settings.oLoadedState = $.extend( true, {}, s );
|
||||||
|
|
||||||
|
// Restore key features - todo - for 1.11 this needs to be done by
|
||||||
|
// subscribed events
|
||||||
|
if ( s.start !== undefined ) {
|
||||||
|
settings._iDisplayStart = s.start;
|
||||||
|
if(api === null) {
|
||||||
|
settings.iInitDisplayStart = s.start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( s.length !== undefined ) {
|
||||||
|
settings._iDisplayLength = s.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order
|
||||||
|
if ( s.order !== undefined ) {
|
||||||
|
settings.aaSorting = [];
|
||||||
|
$.each( s.order, function ( i, col ) {
|
||||||
|
settings.aaSorting.push( col[0] >= columns.length ?
|
||||||
|
[ 0, col[1] ] :
|
||||||
|
col
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
if ( s.search !== undefined ) {
|
||||||
|
$.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Columns
|
||||||
|
if ( s.columns ) {
|
||||||
|
for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
|
||||||
|
var col = s.columns[i];
|
||||||
|
|
||||||
|
// Visibility
|
||||||
|
if ( col.visible !== undefined ) {
|
||||||
|
// If the api is defined, the table has been initialised so we need to use it rather than internal settings
|
||||||
|
if (api) {
|
||||||
|
// Don't redraw the columns on every iteration of this loop, we will do this at the end instead
|
||||||
|
api.column(i).visible(col.visible, false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
columns[i].bVisible = col.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
if ( col.search !== undefined ) {
|
||||||
|
$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the api is defined then we need to adjust the columns once the visibility has been changed
|
||||||
|
if (api) {
|
||||||
|
api.columns.adjust();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings._bLoadingState = false;
|
||||||
|
_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the settings object for a particular table
|
* Return the settings object for a particular table
|
||||||
@@ -9590,7 +9644,7 @@
|
|||||||
* @type string
|
* @type string
|
||||||
* @default Version number
|
* @default Version number
|
||||||
*/
|
*/
|
||||||
DataTable.version = "1.11.2";
|
DataTable.version = "1.11.3";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private data store, containing all of the settings objects that are
|
* Private data store, containing all of the settings objects that are
|
||||||
@@ -14015,7 +14069,7 @@
|
|||||||
*
|
*
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
build:"bs5/dt-1.11.2",
|
build:"bs5/dt-1.11.3",
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15048,6 +15102,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var __htmlEscapeEntities = function ( d ) {
|
var __htmlEscapeEntities = function ( d ) {
|
||||||
|
if (Array.isArray(d)) {
|
||||||
|
d = d.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
return typeof d === 'string' ?
|
return typeof d === 'string' ?
|
||||||
d
|
d
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -15242,6 +15300,7 @@
|
|||||||
_fnSortData: _fnSortData,
|
_fnSortData: _fnSortData,
|
||||||
_fnSaveState: _fnSaveState,
|
_fnSaveState: _fnSaveState,
|
||||||
_fnLoadState: _fnLoadState,
|
_fnLoadState: _fnLoadState,
|
||||||
|
_fnImplementState: _fnImplementState,
|
||||||
_fnSettingsFromNode: _fnSettingsFromNode,
|
_fnSettingsFromNode: _fnSettingsFromNode,
|
||||||
_fnLog: _fnLog,
|
_fnLog: _fnLog,
|
||||||
_fnMap: _fnMap,
|
_fnMap: _fnMap,
|
||||||
|
@@ -150,7 +150,7 @@
|
|||||||
|
|
||||||
<dt class="col-sm-5">Domain configuration
|
<dt class="col-sm-5">Domain configuration
|
||||||
<span class="badge bg-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span>
|
<span class="badge bg-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span>
|
||||||
<span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not matches the browsers location.
The domain variable does not seem to be configured correctly.
Some features may not work as expected!">No Match</span>
|
<span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not match the browser location.
The domain variable does not seem to be configured correctly.
Some features may not work as expected!">No Match</span>
|
||||||
<span class="badge bg-success d-none" id="https-success" title="Configurued to use HTTPS">HTTPS</span>
|
<span class="badge bg-success d-none" id="https-success" title="Configurued to use HTTPS">HTTPS</span>
|
||||||
<span class="badge bg-danger d-none" id="https-warning" title="Not configured to use HTTPS.
Some features may not work as expected!">No HTTPS</span>
|
<span class="badge bg-danger d-none" id="https-warning" title="Not configured to use HTTPS.
Some features may not work as expected!">No HTTPS</span>
|
||||||
</dt>
|
</dt>
|
||||||
|
10
src/static/templates/email/incomplete_2fa_login.hbs
Normal file
10
src/static/templates/email/incomplete_2fa_login.hbs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Incomplete Two-Step Login From {{{device}}}
|
||||||
|
<!---------------->
|
||||||
|
Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt.
|
||||||
|
|
||||||
|
* Date: {{datetime}}
|
||||||
|
* IP Address: {{ip}}
|
||||||
|
* Device Type: {{device}}
|
||||||
|
|
||||||
|
If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised.
|
||||||
|
{{> email/email_footer_text }}
|
31
src/static/templates/email/incomplete_2fa_login.html.hbs
Normal file
31
src/static/templates/email/incomplete_2fa_login.html.hbs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Incomplete Two-Step Login From {{{device}}}
|
||||||
|
<!---------------->
|
||||||
|
{{> email/email_header }}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
<b>Date</b>: {{datetime}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
<b>IP Address:</b> {{ip}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
<b>Device Type:</b> {{device}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{> email/email_footer }}
|
17
src/util.rs
17
src/util.rs
@@ -282,9 +282,9 @@ pub fn delete_file(path: &str) -> IOResult<()> {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
|
|
||||||
|
|
||||||
pub fn get_display_size(size: i32) -> String {
|
pub fn get_display_size(size: i32) -> String {
|
||||||
|
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
|
||||||
|
|
||||||
let mut size: f64 = size.into();
|
let mut size: f64 = size.into();
|
||||||
let mut unit_counter = 0;
|
let mut unit_counter = 0;
|
||||||
|
|
||||||
@@ -359,10 +359,10 @@ where
|
|||||||
try_parse_string(get_env_str_value(key))
|
try_parse_string(get_env_str_value(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"];
|
|
||||||
const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"];
|
|
||||||
|
|
||||||
pub fn get_env_bool(key: &str) -> Option<bool> {
|
pub fn get_env_bool(key: &str) -> Option<bool> {
|
||||||
|
const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"];
|
||||||
|
const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"];
|
||||||
|
|
||||||
match get_env_str_value(key) {
|
match get_env_str_value(key) {
|
||||||
Some(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true),
|
Some(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true),
|
||||||
Some(val) if FALSE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(false),
|
Some(val) if FALSE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(false),
|
||||||
@@ -375,7 +375,6 @@ pub fn get_env_bool(key: &str) -> Option<bool> {
|
|||||||
//
|
//
|
||||||
|
|
||||||
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
|
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
|
||||||
use chrono_tz::Tz;
|
|
||||||
|
|
||||||
/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API
|
/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API
|
||||||
/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.).
|
/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.).
|
||||||
@@ -393,7 +392,7 @@ pub fn format_datetime_local(dt: &DateTime<Local>, fmt: &str) -> String {
|
|||||||
// Try parsing the `TZ` environment variable to enable formatting `%Z` as
|
// Try parsing the `TZ` environment variable to enable formatting `%Z` as
|
||||||
// a time zone abbreviation.
|
// a time zone abbreviation.
|
||||||
if let Ok(tz) = env::var("TZ") {
|
if let Ok(tz) = env::var("TZ") {
|
||||||
if let Ok(tz) = tz.parse::<Tz>() {
|
if let Ok(tz) = tz.parse::<chrono_tz::Tz>() {
|
||||||
return dt.with_timezone(&tz).format(fmt).to_string();
|
return dt.with_timezone(&tz).format(fmt).to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,7 +441,7 @@ use serde_json::{self, Value};
|
|||||||
|
|
||||||
pub type JsonMap = serde_json::Map<String, Value>;
|
pub type JsonMap = serde_json::Map<String, Value>;
|
||||||
|
|
||||||
#[derive(PartialEq, Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct UpCase<T: DeserializeOwned> {
|
pub struct UpCase<T: DeserializeOwned> {
|
||||||
#[serde(deserialize_with = "upcase_deserialize")]
|
#[serde(deserialize_with = "upcase_deserialize")]
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@@ -517,6 +516,8 @@ fn upcase_value(value: Value) -> Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inner function to handle some speciale case for the 'ssn' key.
|
||||||
|
// This key is part of the Identity Cipher (Social Security Number)
|
||||||
fn _process_key(key: &str) -> String {
|
fn _process_key(key: &str) -> String {
|
||||||
match key.to_lowercase().as_ref() {
|
match key.to_lowercase().as_ref() {
|
||||||
"ssn" => "SSN".into(),
|
"ssn" => "SSN".into(),
|
||||||
|
Reference in New Issue
Block a user