mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 18:55:57 +03:00
Compare commits
46 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1aa5e0d4dc | ||
|
b47cf97409 | ||
|
5e802f8aa3 | ||
|
0bdeb02a31 | ||
|
b03698fadb | ||
|
39d1a09704 | ||
|
a447e4e7ef | ||
|
4eee6e7aee | ||
|
b6fde857a7 | ||
|
3c66deb5cc | ||
|
4146612a32 | ||
|
a314933557 | ||
|
c5d7e3f2bc | ||
|
c95a2881b5 | ||
|
4c3727b4a3 | ||
|
a1f304dff7 | ||
|
a8870eef0d | ||
|
afaebc6cf3 | ||
|
8f4a1f4fc2 | ||
|
0807783388 | ||
|
80d4061d14 | ||
|
dc2f8e5c85 | ||
|
aee1ea032b | ||
|
484e82fb9f | ||
|
322a08edfb | ||
|
08afc312c3 | ||
|
5571a5d8ed | ||
|
6a8c65493f | ||
|
dfdf4473ea | ||
|
8bbbff7567 | ||
|
42e37ebea1 | ||
|
632f4d5453 | ||
|
6c5e35ce5c | ||
|
4ff15f6dc2 | ||
|
ec8028aef2 | ||
|
63cbd9ef9c | ||
|
9cca64003a | ||
|
819d5e2dc8 | ||
|
3b06ab296b | ||
|
0de52c6c99 | ||
|
e3b00b59a7 | ||
|
5a390a973f | ||
|
1ee8e44912 | ||
|
86685c1cd2 | ||
|
e4d08836e2 | ||
|
c2a324e5da |
740
Cargo.lock
generated
740
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@@ -26,7 +26,7 @@ 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.10.4", features = ["blocking", "json"] }
|
reqwest = { version = "0.10.6", features = ["blocking", "json"] }
|
||||||
|
|
||||||
# multipart/form-data support
|
# multipart/form-data support
|
||||||
multipart = { version = "0.16.1", features = ["server"], default-features = false }
|
multipart = { version = "0.16.1", features = ["server"], default-features = false }
|
||||||
@@ -41,9 +41,9 @@ rmpv = "0.4.4"
|
|||||||
chashmap = "2.2.2"
|
chashmap = "2.2.2"
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = "1.0.106"
|
serde = "1.0.111"
|
||||||
serde_derive = "1.0.106"
|
serde_derive = "1.0.111"
|
||||||
serde_json = "1.0.51"
|
serde_json = "1.0.53"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
@@ -57,20 +57,20 @@ diesel_migrations = "1.4.0"
|
|||||||
libsqlite3-sys = { version = "0.17.3", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.17.3", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto library
|
# Crypto library
|
||||||
ring = "0.16.12"
|
ring = "0.16.14"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "0.8.1", features = ["v4"] }
|
uuid = { version = "0.8.1", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time librar for Rust
|
# Date and time librar for Rust
|
||||||
chrono = "0.4.11"
|
chrono = "0.4.11"
|
||||||
time = "0.2.9"
|
time = "0.2.16"
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
oath = "0.10.2"
|
oath = "0.10.2"
|
||||||
|
|
||||||
# Data encoding library
|
# Data encoding library
|
||||||
data-encoding = "2.2.0"
|
data-encoding = "2.2.1"
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = "7.1.0"
|
jsonwebtoken = "7.1.0"
|
||||||
@@ -85,26 +85,22 @@ yubico = { version = "0.9.0", features = ["online-tokio"], default-features = fa
|
|||||||
dotenv = { version = "0.15.0", default-features = false }
|
dotenv = { version = "0.15.0", default-features = false }
|
||||||
|
|
||||||
# Lazy initialization
|
# Lazy initialization
|
||||||
once_cell = "1.3.1"
|
once_cell = "1.4.0"
|
||||||
|
|
||||||
# More derives
|
|
||||||
derive_more = "0.99.5"
|
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.11"
|
num-traits = "0.2.11"
|
||||||
num-derive = "0.3.0"
|
num-derive = "0.3.0"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = "0.10.0-pre"
|
lettre = { version = "0.10.0-alpha.1", features = ["smtp-transport", "builder", "serde", "native-tls"], default-features = false }
|
||||||
native-tls = "0.2.4"
|
native-tls = "0.2.4"
|
||||||
quoted_printable = "0.4.2"
|
|
||||||
|
|
||||||
# Template library
|
# Template library
|
||||||
handlebars = { version = "3.0.1", features = ["dir_source"] }
|
handlebars = { version = "3.0.1", features = ["dir_source"] }
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# For favicon extraction from main website
|
||||||
soup = "0.5.0"
|
soup = "0.5.0"
|
||||||
regex = "1.3.6"
|
regex = "1.3.9"
|
||||||
data-url = "0.1.0"
|
data-url = "0.1.0"
|
||||||
|
|
||||||
# Used by U2F, JWT and Postgres
|
# Used by U2F, JWT and Postgres
|
||||||
@@ -116,18 +112,15 @@ percent-encoding = "2.1.0"
|
|||||||
idna = "0.2.0"
|
idna = "0.2.0"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
structopt = "0.3.13"
|
structopt = "0.3.14"
|
||||||
|
|
||||||
# Logging panics to logfile instead stderr only
|
# Logging panics to logfile instead stderr only
|
||||||
backtrace = "0.3.46"
|
backtrace = "0.3.48"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Use newest ring
|
# Use newest ring
|
||||||
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dfc9e9aab01d349da32c52db393e35b7fffea63c' }
|
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '1010f6a2a88fac899dec0cd2f642156908038a53' }
|
||||||
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dfc9e9aab01d349da32c52db393e35b7fffea63c' }
|
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = '1010f6a2a88fac899dec0cd2f642156908038a53' }
|
||||||
|
|
||||||
# Use git version for timeout fix #706
|
|
||||||
lettre = { git = 'https://github.com/lettre/lettre', rev = '245c600c82ee18b766e8729f005ff453a55dce34' }
|
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# For favicon extraction from main website
|
||||||
data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = '7f1bd6ce1c2fde599a757302a843a60e714c5f72' }
|
data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = '7f1bd6ce1c2fde599a757302a843a60e714c5f72' }
|
||||||
|
@@ -27,17 +27,17 @@
|
|||||||
# 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_image_hash = "sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554" %}
|
{% set vault_image_hash = "sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5" %}
|
||||||
{% raw %}
|
{% raw %}
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
FROM bitwardenrs/web-vault@{{ vault_image_hash }} as vault
|
FROM bitwardenrs/web-vault@{{ vault_image_hash }} as vault
|
||||||
|
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# Musl build image for statically compiled binary
|
# Musl build image for statically compiled binary
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# Musl build image for statically compiled binary
|
# Musl build image for statically compiled binary
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# Musl build image for statically compiled binary
|
# Musl build image for statically compiled binary
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.13.2b
|
# docker pull bitwardenrs/web-vault:v2.14.0
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.13.2b
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.14.0
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5
|
||||||
FROM bitwardenrs/web-vault@sha256:f32c555a2bc3ee6bc0718319b1e8057c10ef889cf7231f0ff217af98486da554 as vault
|
FROM bitwardenrs/web-vault@sha256:c62e0b8698562e03fe2759374f3ecd760d9612e4eaf0af4583f231ebf05d6df5 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE ciphers
|
||||||
|
ADD COLUMN
|
||||||
|
deleted_at DATETIME;
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE ciphers
|
||||||
|
ADD COLUMN
|
||||||
|
deleted_at TIMESTAMP;
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE ciphers
|
||||||
|
ADD COLUMN
|
||||||
|
deleted_at DATETIME;
|
@@ -1 +1 @@
|
|||||||
nightly-2020-04-08
|
nightly-2020-05-31
|
187
src/api/admin.rs
187
src/api/admin.rs
@@ -23,7 +23,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
|
|
||||||
routes![
|
routes![
|
||||||
admin_login,
|
admin_login,
|
||||||
get_users,
|
get_users_json,
|
||||||
post_admin_login,
|
post_admin_login,
|
||||||
admin_page,
|
admin_page,
|
||||||
invite_user,
|
invite_user,
|
||||||
@@ -36,6 +36,9 @@ pub fn routes() -> Vec<Route> {
|
|||||||
delete_config,
|
delete_config,
|
||||||
backup_db,
|
backup_db,
|
||||||
test_smtp,
|
test_smtp,
|
||||||
|
users_overview,
|
||||||
|
organizations_overview,
|
||||||
|
diagnostics,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +60,14 @@ fn admin_path() -> String {
|
|||||||
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used for `Location` response headers, which must specify an absolute URI
|
||||||
|
/// (see https://tools.ietf.org/html/rfc2616#section-14.30).
|
||||||
|
fn admin_url() -> String {
|
||||||
|
// Don't use CONFIG.domain() directly, since the user may want to keep a
|
||||||
|
// trailing slash there, particularly when running under a subpath.
|
||||||
|
format!("{}{}{}", CONFIG.domain_origin(), CONFIG.domain_path(), ADMIN_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/", rank = 2)]
|
#[get("/", rank = 2)]
|
||||||
fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
|
fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
|
||||||
// If there is an error, show it
|
// If there is an error, show it
|
||||||
@@ -81,7 +92,7 @@ fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -
|
|||||||
if !_validate_token(&data.token) {
|
if !_validate_token(&data.token) {
|
||||||
error!("Invalid admin token. IP: {}", ip.ip);
|
error!("Invalid admin token. IP: {}", ip.ip);
|
||||||
Err(Flash::error(
|
Err(Flash::error(
|
||||||
Redirect::to(admin_path()),
|
Redirect::to(admin_url()),
|
||||||
"Invalid admin token, please try again.",
|
"Invalid admin token, please try again.",
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
@@ -97,7 +108,7 @@ fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -
|
|||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
cookies.add(cookie);
|
cookies.add(cookie);
|
||||||
Ok(Redirect::to(admin_path()))
|
Ok(Redirect::to(admin_url()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +123,9 @@ fn _validate_token(token: &str) -> bool {
|
|||||||
struct AdminTemplateData {
|
struct AdminTemplateData {
|
||||||
page_content: String,
|
page_content: String,
|
||||||
version: Option<&'static str>,
|
version: Option<&'static str>,
|
||||||
users: Vec<Value>,
|
users: Option<Vec<Value>>,
|
||||||
|
organizations: Option<Vec<Value>>,
|
||||||
|
diagnostics: Option<Value>,
|
||||||
config: Value,
|
config: Value,
|
||||||
can_backup: bool,
|
can_backup: bool,
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
@@ -120,15 +133,59 @@ struct AdminTemplateData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AdminTemplateData {
|
impl AdminTemplateData {
|
||||||
fn new(users: Vec<Value>) -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
page_content: String::from("admin/page"),
|
page_content: String::from("admin/settings"),
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
users,
|
|
||||||
config: CONFIG.prepare_json(),
|
config: CONFIG.prepare_json(),
|
||||||
can_backup: *CAN_BACKUP,
|
can_backup: *CAN_BACKUP,
|
||||||
logged_in: true,
|
logged_in: true,
|
||||||
urlpath: CONFIG.domain_path(),
|
urlpath: CONFIG.domain_path(),
|
||||||
|
users: None,
|
||||||
|
organizations: None,
|
||||||
|
diagnostics: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn users(users: Vec<Value>) -> Self {
|
||||||
|
Self {
|
||||||
|
page_content: String::from("admin/users"),
|
||||||
|
version: VERSION,
|
||||||
|
users: Some(users),
|
||||||
|
config: CONFIG.prepare_json(),
|
||||||
|
can_backup: *CAN_BACKUP,
|
||||||
|
logged_in: true,
|
||||||
|
urlpath: CONFIG.domain_path(),
|
||||||
|
organizations: None,
|
||||||
|
diagnostics: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn organizations(organizations: Vec<Value>) -> Self {
|
||||||
|
Self {
|
||||||
|
page_content: String::from("admin/organizations"),
|
||||||
|
version: VERSION,
|
||||||
|
organizations: Some(organizations),
|
||||||
|
config: CONFIG.prepare_json(),
|
||||||
|
can_backup: *CAN_BACKUP,
|
||||||
|
logged_in: true,
|
||||||
|
urlpath: CONFIG.domain_path(),
|
||||||
|
users: None,
|
||||||
|
diagnostics: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnostics(diagnostics: Value) -> Self {
|
||||||
|
Self {
|
||||||
|
page_content: String::from("admin/diagnostics"),
|
||||||
|
version: VERSION,
|
||||||
|
organizations: None,
|
||||||
|
config: CONFIG.prepare_json(),
|
||||||
|
can_backup: *CAN_BACKUP,
|
||||||
|
logged_in: true,
|
||||||
|
urlpath: CONFIG.domain_path(),
|
||||||
|
users: None,
|
||||||
|
diagnostics: Some(diagnostics),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +195,8 @@ impl AdminTemplateData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/", rank = 1)]
|
#[get("/", rank = 1)]
|
||||||
fn admin_page(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
fn admin_page(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
let users = User::get_all(&conn);
|
let text = AdminTemplateData::new().render()?;
|
||||||
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
|
||||||
|
|
||||||
let text = AdminTemplateData::new(users_json).render()?;
|
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,10 +228,9 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
|
|||||||
#[post("/test/smtp", data = "<data>")]
|
#[post("/test/smtp", data = "<data>")]
|
||||||
fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
||||||
let data: InviteData = data.into_inner();
|
let data: InviteData = data.into_inner();
|
||||||
let email = data.email.clone();
|
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_test(&email)
|
mail::send_test(&data.email)
|
||||||
} else {
|
} else {
|
||||||
err!("Mail is not enabled")
|
err!("Mail is not enabled")
|
||||||
}
|
}
|
||||||
@@ -186,17 +239,33 @@ fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
|||||||
#[get("/logout")]
|
#[get("/logout")]
|
||||||
fn logout(mut cookies: Cookies) -> Result<Redirect, ()> {
|
fn logout(mut cookies: Cookies) -> Result<Redirect, ()> {
|
||||||
cookies.remove(Cookie::named(COOKIE_NAME));
|
cookies.remove(Cookie::named(COOKIE_NAME));
|
||||||
Ok(Redirect::to(admin_path()))
|
Ok(Redirect::to(admin_url()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users")]
|
#[get("/users")]
|
||||||
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
|
fn get_users_json(_token: AdminToken, conn: DbConn) -> JsonResult {
|
||||||
let users = User::get_all(&conn);
|
let users = User::get_all(&conn);
|
||||||
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
||||||
|
|
||||||
Ok(Json(Value::Array(users_json)))
|
Ok(Json(Value::Array(users_json)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/users/overview")]
|
||||||
|
fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
|
let users = User::get_all(&conn);
|
||||||
|
let users_json: Vec<Value> = users.iter()
|
||||||
|
.map(|u| {
|
||||||
|
let mut usr = u.to_json(&conn);
|
||||||
|
if let Some(ciphers) = Cipher::count_owned_by_user(&u.uuid, &conn) {
|
||||||
|
usr["cipher_count"] = json!(ciphers);
|
||||||
|
};
|
||||||
|
usr
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let text = AdminTemplateData::users(users_json).render()?;
|
||||||
|
Ok(Html(text))
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/delete")]
|
#[post("/users/<uuid>/delete")]
|
||||||
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
let user = match User::find_by_uuid(&uuid, &conn) {
|
let user = match User::find_by_uuid(&uuid, &conn) {
|
||||||
@@ -237,6 +306,92 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
|||||||
User::update_all_revisions(&conn)
|
User::update_all_revisions(&conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/organizations/overview")]
|
||||||
|
fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
|
let organizations = Organization::get_all(&conn);
|
||||||
|
let organizations_json: Vec<Value> = organizations.iter().map(|o| o.to_json()).collect();
|
||||||
|
|
||||||
|
let text = AdminTemplateData::organizations(organizations_json).render()?;
|
||||||
|
Ok(Html(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct WebVaultVersion {
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_github_api(url: &str) -> Result<Value, Error> {
|
||||||
|
use reqwest::{header::USER_AGENT, blocking::Client};
|
||||||
|
let github_api = Client::builder().build()?;
|
||||||
|
|
||||||
|
let res = github_api
|
||||||
|
.get(url)
|
||||||
|
.header(USER_AGENT, "Bitwarden_RS")
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
let res_status = res.status();
|
||||||
|
if res_status != 200 {
|
||||||
|
error!("Could not retrieve '{}', response code: {}", url, res_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: Value = res.error_for_status()?.json()?;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/diagnostics")]
|
||||||
|
fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use crate::util::read_file_string;
|
||||||
|
|
||||||
|
let vault_version_path = format!("{}/{}", CONFIG.web_vault_folder(), "version.json");
|
||||||
|
let vault_version_str = read_file_string(&vault_version_path)?;
|
||||||
|
let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?;
|
||||||
|
|
||||||
|
let github_ips = ("github.com", 0).to_socket_addrs().map(|mut i| i.next());
|
||||||
|
let dns_resolved = match github_ips {
|
||||||
|
Ok(Some(a)) => a.ip().to_string(),
|
||||||
|
_ => "Could not resolve domain name.".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let bitwarden_rs_releases = get_github_api("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest");
|
||||||
|
let latest_release = match &bitwarden_rs_releases {
|
||||||
|
Ok(j) => j["tag_name"].as_str().unwrap(),
|
||||||
|
_ => "-",
|
||||||
|
};
|
||||||
|
|
||||||
|
let bitwarden_rs_commits = get_github_api("https://api.github.com/repos/dani-garcia/bitwarden_rs/commits/master");
|
||||||
|
let mut latest_commit = match &bitwarden_rs_commits {
|
||||||
|
Ok(j) => j["sha"].as_str().unwrap(),
|
||||||
|
_ => "-",
|
||||||
|
};
|
||||||
|
if latest_commit.len() >= 8 {
|
||||||
|
latest_commit = &latest_commit[..8];
|
||||||
|
}
|
||||||
|
|
||||||
|
let bw_web_builds_releases = get_github_api("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest");
|
||||||
|
let latest_web_build = match &bw_web_builds_releases {
|
||||||
|
Ok(j) => j["tag_name"].as_str().unwrap(),
|
||||||
|
_ => "-",
|
||||||
|
};
|
||||||
|
|
||||||
|
let dt = Utc::now();
|
||||||
|
let server_time = dt.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
|
||||||
|
let diagnostics_json = json!({
|
||||||
|
"dns_resolved": dns_resolved,
|
||||||
|
"server_time": server_time,
|
||||||
|
"web_vault_version": web_vault_version.version,
|
||||||
|
"latest_release": latest_release,
|
||||||
|
"latest_commit": latest_commit,
|
||||||
|
"latest_web_build": latest_web_build.replace("v", ""),
|
||||||
|
});
|
||||||
|
|
||||||
|
let text = AdminTemplateData::diagnostics(diagnostics_json).render()?;
|
||||||
|
Ok(Html(text))
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/config", data = "<data>")]
|
#[post("/config", data = "<data>")]
|
||||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||||
let data: ConfigBuilder = data.into_inner();
|
let data: ConfigBuilder = data.into_inner();
|
||||||
|
@@ -68,7 +68,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
let mut user = match User::find_by_mail(&data.Email, &conn) {
|
let mut user = match User::find_by_mail(&data.Email, &conn) {
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
if !user.password_hash.is_empty() {
|
if !user.password_hash.is_empty() {
|
||||||
if CONFIG.signups_allowed() {
|
if CONFIG.is_signup_allowed(&data.Email) {
|
||||||
err!("User already exists")
|
err!("User already exists")
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
@@ -89,14 +89,17 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user
|
user
|
||||||
} else if CONFIG.signups_allowed() {
|
} else if CONFIG.is_signup_allowed(&data.Email) {
|
||||||
err!("Account with this email already exists")
|
err!("Account with this email already exists")
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if CONFIG.signups_allowed() || Invitation::take(&data.Email, &conn) || CONFIG.can_signup_user(&data.Email) {
|
// Order is important here; the invitation check must come first
|
||||||
|
// because the bitwarden_rs admin can invite anyone, regardless
|
||||||
|
// of other signup restrictions.
|
||||||
|
if Invitation::take(&data.Email, &conn) || CONFIG.is_signup_allowed(&data.Email) {
|
||||||
User::new(data.Email.clone())
|
User::new(data.Email.clone())
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
@@ -207,7 +210,12 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
|
|||||||
user.public_key = Some(data.PublicKey);
|
user.public_key = Some(data.PublicKey);
|
||||||
|
|
||||||
user.save(&conn)?;
|
user.save(&conn)?;
|
||||||
Ok(Json(user.to_json(&conn)))
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"PrivateKey": user.private_key,
|
||||||
|
"PublicKey": user.public_key,
|
||||||
|
"Object":"keys"
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -371,8 +379,8 @@ fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: Db
|
|||||||
err!("Email already in use");
|
err!("Email already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CONFIG.signups_allowed() && !CONFIG.can_signup_user(&data.NewEmail) {
|
if !CONFIG.is_email_domain_allowed(&data.NewEmail) {
|
||||||
err!("Email cannot be changed to this address");
|
err!("Email domain not allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = crypto::generate_token(6)?;
|
let token = crypto::generate_token(6)?;
|
||||||
|
@@ -49,10 +49,16 @@ pub fn routes() -> Vec<Route> {
|
|||||||
put_cipher,
|
put_cipher,
|
||||||
delete_cipher_post,
|
delete_cipher_post,
|
||||||
delete_cipher_post_admin,
|
delete_cipher_post_admin,
|
||||||
|
delete_cipher_put,
|
||||||
|
delete_cipher_put_admin,
|
||||||
delete_cipher,
|
delete_cipher,
|
||||||
delete_cipher_admin,
|
delete_cipher_admin,
|
||||||
delete_cipher_selected,
|
delete_cipher_selected,
|
||||||
delete_cipher_selected_post,
|
delete_cipher_selected_post,
|
||||||
|
delete_cipher_selected_put,
|
||||||
|
restore_cipher_put,
|
||||||
|
restore_cipher_put_admin,
|
||||||
|
restore_cipher_selected,
|
||||||
delete_all,
|
delete_all,
|
||||||
move_cipher_selected,
|
move_cipher_selected,
|
||||||
move_cipher_selected_put,
|
move_cipher_selected_put,
|
||||||
@@ -761,11 +767,7 @@ fn post_attachment_admin(
|
|||||||
post_attachment(uuid, data, content_type, headers, conn, nt)
|
post_attachment(uuid, data, content_type, headers, conn, nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post(
|
#[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
|
||||||
"/ciphers/<uuid>/attachment/<attachment_id>/share",
|
|
||||||
format = "multipart/form-data",
|
|
||||||
data = "<data>"
|
|
||||||
)]
|
|
||||||
fn post_attachment_share(
|
fn post_attachment_share(
|
||||||
uuid: String,
|
uuid: String,
|
||||||
attachment_id: String,
|
attachment_id: String,
|
||||||
@@ -819,48 +821,62 @@ fn delete_attachment_admin(
|
|||||||
|
|
||||||
#[post("/ciphers/<uuid>/delete")]
|
#[post("/ciphers/<uuid>/delete")]
|
||||||
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/delete-admin")]
|
#[post("/ciphers/<uuid>/delete-admin")]
|
||||||
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/<uuid>/delete")]
|
||||||
|
fn delete_cipher_put(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, true, &nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/<uuid>/delete-admin")]
|
||||||
|
fn delete_cipher_put_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, true, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>")]
|
#[delete("/ciphers/<uuid>")]
|
||||||
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>/admin")]
|
#[delete("/ciphers/<uuid>/admin")]
|
||||||
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers", data = "<data>")]
|
#[delete("/ciphers", data = "<data>")]
|
||||||
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
let data: Value = data.into_inner().data;
|
_delete_multiple_ciphers(data, headers, conn, false, nt)
|
||||||
|
|
||||||
let uuids = match data.get("Ids") {
|
|
||||||
Some(ids) => match ids.as_array() {
|
|
||||||
Some(ids) => ids.iter().filter_map(Value::as_str),
|
|
||||||
None => err!("Posted ids field is not an array"),
|
|
||||||
},
|
|
||||||
None => err!("Request missing ids field"),
|
|
||||||
};
|
|
||||||
|
|
||||||
for uuid in uuids {
|
|
||||||
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, &nt) {
|
|
||||||
return error;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/delete", data = "<data>")]
|
#[post("/ciphers/delete", data = "<data>")]
|
||||||
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
delete_cipher_selected(data, headers, conn, nt)
|
_delete_multiple_ciphers(data, headers, conn, false, nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/delete", data = "<data>")]
|
||||||
|
fn delete_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_delete_multiple_ciphers(data, headers, conn, true, nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/<uuid>/restore")]
|
||||||
|
fn restore_cipher_put(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/<uuid>/restore-admin")]
|
||||||
|
fn restore_cipher_put_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/restore", data = "<data>")]
|
||||||
|
fn restore_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_restore_multiple_ciphers(data, headers, conn, nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -974,8 +990,8 @@ fn delete_all(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
|
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, soft_delete: bool, nt: &Notify) -> EmptyResult {
|
||||||
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
||||||
Some(cipher) => cipher,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
None => err!("Cipher doesn't exist"),
|
||||||
};
|
};
|
||||||
@@ -984,11 +1000,74 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Not
|
|||||||
err!("Cipher can't be deleted by user")
|
err!("Cipher can't be deleted by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
cipher.delete(&conn)?;
|
if soft_delete {
|
||||||
|
cipher.deleted_at = Some(chrono::Utc::now().naive_utc());
|
||||||
|
cipher.save(&conn)?;
|
||||||
|
} else {
|
||||||
|
cipher.delete(&conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn _delete_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, soft_delete: bool, nt: Notify) -> EmptyResult {
|
||||||
|
let data: Value = data.into_inner().data;
|
||||||
|
|
||||||
|
let uuids = match data.get("Ids") {
|
||||||
|
Some(ids) => match ids.as_array() {
|
||||||
|
Some(ids) => ids.iter().filter_map(Value::as_str),
|
||||||
|
None => err!("Posted ids field is not an array"),
|
||||||
|
},
|
||||||
|
None => err!("Request missing ids field"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for uuid in uuids {
|
||||||
|
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, soft_delete, &nt) {
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
|
||||||
|
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
||||||
|
Some(cipher) => cipher,
|
||||||
|
None => err!("Cipher doesn't exist"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
|
||||||
|
err!("Cipher can't be restored by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.deleted_at = None;
|
||||||
|
cipher.save(&conn)?;
|
||||||
|
|
||||||
|
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _restore_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
let data: Value = data.into_inner().data;
|
||||||
|
|
||||||
|
let uuids = match data.get("Ids") {
|
||||||
|
Some(ids) => match ids.as_array() {
|
||||||
|
Some(ids) => ids.iter().filter_map(Value::as_str),
|
||||||
|
None => err!("Posted ids field is not an array"),
|
||||||
|
},
|
||||||
|
None => err!("Request missing ids field"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for uuid in uuids {
|
||||||
|
if let error @ Err(_) = _restore_cipher_by_uuid(uuid, &headers, &conn, &nt) {
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn _delete_cipher_attachment_by_id(
|
fn _delete_cipher_attachment_by_id(
|
||||||
uuid: &str,
|
uuid: &str,
|
||||||
attachment_id: &str,
|
attachment_id: &str,
|
||||||
|
@@ -50,7 +50,6 @@ fn get_folder(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
|
||||||
pub struct FolderData {
|
pub struct FolderData {
|
||||||
pub Name: String,
|
pub Name: String,
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ mod accounts;
|
|||||||
mod ciphers;
|
mod ciphers;
|
||||||
mod folders;
|
mod folders;
|
||||||
mod organizations;
|
mod organizations;
|
||||||
pub(crate) mod two_factor;
|
pub mod two_factor;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut mod_routes = routes![
|
let mut mod_routes = routes![
|
||||||
|
@@ -374,7 +374,7 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
|
|||||||
.map(|col_user| {
|
.map(|col_user| {
|
||||||
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
|
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_json_collection_user_details(col_user.read_only)
|
.to_json_read_only(col_user.read_only)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -485,7 +485,11 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
|||||||
let user = match User::find_by_mail(&email, &conn) {
|
let user = match User::find_by_mail(&email, &conn) {
|
||||||
None => {
|
None => {
|
||||||
if !CONFIG.invitations_allowed() {
|
if !CONFIG.invitations_allowed() {
|
||||||
err!(format!("User email does not exist: {}", email))
|
err!(format!("User does not exist: {}", email))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !CONFIG.is_email_domain_allowed(&email) {
|
||||||
|
err!("Email domain not eligible for invitations")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CONFIG.mail_enabled() {
|
if !CONFIG.mail_enabled() {
|
||||||
|
@@ -4,7 +4,7 @@ use rocket_contrib::json::Json;
|
|||||||
|
|
||||||
use crate::api::core::two_factor::_generate_recover_code;
|
use crate::api::core::two_factor::_generate_recover_code;
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||||
use crate::auth::Headers;
|
use crate::auth::{ClientIp, Headers};
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{TwoFactor, TwoFactorType},
|
||||||
@@ -20,6 +20,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
activate_authenticator_put,
|
activate_authenticator_put,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-authenticator", data = "<data>")]
|
#[post("/two-factor/get-authenticator", data = "<data>")]
|
||||||
fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
@@ -53,7 +54,12 @@ struct EnableAuthenticatorData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/authenticator", data = "<data>")]
|
#[post("/two-factor/authenticator", data = "<data>")]
|
||||||
fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn activate_authenticator(
|
||||||
|
data: JsonUpcase<EnableAuthenticatorData>,
|
||||||
|
headers: Headers,
|
||||||
|
ip: ClientIp,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
let data: EnableAuthenticatorData = data.into_inner().data;
|
let data: EnableAuthenticatorData = data.into_inner().data;
|
||||||
let password_hash = data.MasterPasswordHash;
|
let password_hash = data.MasterPasswordHash;
|
||||||
let key = data.Key;
|
let key = data.Key;
|
||||||
@@ -76,7 +82,7 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate the token provided with the key, and save new twofactor
|
// Validate the token provided with the key, and save new twofactor
|
||||||
validate_totp_code(&user.uuid, token, &key.to_uppercase(), &conn)?;
|
validate_totp_code(&user.uuid, token, &key.to_uppercase(), &ip, &conn)?;
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &conn);
|
||||||
|
|
||||||
@@ -88,20 +94,31 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/two-factor/authenticator", data = "<data>")]
|
#[put("/two-factor/authenticator", data = "<data>")]
|
||||||
fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn activate_authenticator_put(
|
||||||
activate_authenticator(data, headers, conn)
|
data: JsonUpcase<EnableAuthenticatorData>,
|
||||||
|
headers: Headers,
|
||||||
|
ip: ClientIp,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
activate_authenticator(data, headers, ip, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_totp_code_str(user_uuid: &str, totp_code: &str, secret: &str, conn: &DbConn) -> EmptyResult {
|
pub fn validate_totp_code_str(
|
||||||
|
user_uuid: &str,
|
||||||
|
totp_code: &str,
|
||||||
|
secret: &str,
|
||||||
|
ip: &ClientIp,
|
||||||
|
conn: &DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
let totp_code: u64 = match totp_code.parse() {
|
let totp_code: u64 = match totp_code.parse() {
|
||||||
Ok(code) => code,
|
Ok(code) => code,
|
||||||
_ => err!("TOTP code is not a number"),
|
_ => err!("TOTP code is not a number"),
|
||||||
};
|
};
|
||||||
|
|
||||||
validate_totp_code(user_uuid, totp_code, secret, &conn)
|
validate_totp_code(user_uuid, totp_code, secret, ip, &conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &DbConn) -> EmptyResult {
|
pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, ip: &ClientIp, conn: &DbConn) -> EmptyResult {
|
||||||
use oath::{totp_raw_custom_time, HashType};
|
use oath::{totp_raw_custom_time, HashType};
|
||||||
|
|
||||||
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
||||||
@@ -143,11 +160,22 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &
|
|||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if generated == totp_code && time_step <= twofactor.last_used as i64 {
|
} else if generated == totp_code && time_step <= twofactor.last_used as i64 {
|
||||||
warn!("This or a TOTP code within {} steps back and forward has already been used!", steps);
|
warn!(
|
||||||
err!(format!("Invalid TOTP code! Server time: {}", current_time.format("%F %T UTC")));
|
"This or a TOTP code within {} steps back and forward has already been used!",
|
||||||
|
steps
|
||||||
|
);
|
||||||
|
err!(format!(
|
||||||
|
"Invalid TOTP code! Server time: {} IP: {}",
|
||||||
|
current_time.format("%F %T UTC"),
|
||||||
|
ip.ip
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Else no valide code received, deny access
|
// Else no valide code received, deny access
|
||||||
err!(format!("Invalid TOTP code! Server time: {}", current_time.format("%F %T UTC")));
|
err!(format!(
|
||||||
|
"Invalid TOTP code! Server time: {} IP: {}",
|
||||||
|
current_time.format("%F %T UTC"),
|
||||||
|
ip.ip
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ use chrono::Utc;
|
|||||||
use data_encoding::BASE64;
|
use data_encoding::BASE64;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use crate::api::core::two_factor::_generate_recover_code;
|
use crate::api::core::two_factor::_generate_recover_code;
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use crate::api::core::two_factor::_generate_recover_code;
|
use crate::api::core::two_factor::_generate_recover_code;
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
use data_encoding::BASE32;
|
use data_encoding::BASE32;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::api::{JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
use crate::api::{JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||||
@@ -12,11 +11,11 @@ use crate::db::{
|
|||||||
DbConn,
|
DbConn,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod authenticator;
|
pub mod authenticator;
|
||||||
pub(crate) mod duo;
|
pub mod duo;
|
||||||
pub(crate) mod email;
|
pub mod email;
|
||||||
pub(crate) mod u2f;
|
pub mod u2f;
|
||||||
pub(crate) mod yubikey;
|
pub mod yubikey;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut routes = routes![
|
let mut routes = routes![
|
||||||
@@ -39,7 +38,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
#[get("/two-factor")]
|
#[get("/two-factor")]
|
||||||
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
||||||
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect();
|
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": twofactors_json,
|
"Data": twofactors_json,
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
|
use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
|
||||||
use u2f::protocol::{Challenge, U2f};
|
use u2f::protocol::{Challenge, U2f};
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use yubico::config::Config;
|
use yubico::config::Config;
|
||||||
use yubico::verify;
|
use yubico::verify;
|
||||||
|
@@ -182,7 +182,7 @@ struct Icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
fn new(priority: u8, href: String) -> Self {
|
const fn new(priority: u8, href: String) -> Self {
|
||||||
Self { href, priority }
|
Self { href, priority }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
|||||||
let mut cookie_str = String::new();
|
let mut cookie_str = String::new();
|
||||||
|
|
||||||
let resp = get_page(&ssldomain).or_else(|_| get_page(&httpdomain));
|
let resp = get_page(&ssldomain).or_else(|_| get_page(&httpdomain));
|
||||||
if let Ok(mut content) = resp {
|
if let Ok(content) = resp {
|
||||||
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
||||||
let url = content.url().clone();
|
let url = content.url().clone();
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
|||||||
|
|
||||||
// 512KB should be more than enough for the HTML, though as we only really need
|
// 512KB should be more than enough for the HTML, though as we only really need
|
||||||
// the HTML header, it could potentially be reduced even further
|
// the HTML header, it could potentially be reduced even further
|
||||||
let limited_reader = crate::util::LimitedReader::new(&mut content, 512 * 1024);
|
let limited_reader = content.take(512 * 1024);
|
||||||
|
|
||||||
let soup = Soup::from_reader(limited_reader)?;
|
let soup = Soup::from_reader(limited_reader)?;
|
||||||
// Search for and filter
|
// Search for and filter
|
||||||
|
@@ -38,7 +38,7 @@ fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult {
|
|||||||
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_password_login(data, conn, ip)
|
_password_login(data, conn, &ip)
|
||||||
}
|
}
|
||||||
t => err!("Invalid type", t),
|
t => err!("Invalid type", t),
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult {
|
fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
let scope = data.scope.as_ref().unwrap();
|
let scope = data.scope.as_ref().unwrap();
|
||||||
if scope != "api offline_access" {
|
if scope != "api offline_access" {
|
||||||
@@ -127,7 +127,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
|
|||||||
|
|
||||||
let (mut device, new_device) = get_device(&data, &conn, &user);
|
let (mut device, new_device) = get_device(&data, &conn, &user);
|
||||||
|
|
||||||
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;
|
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &ip, &conn)?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() && new_device {
|
if CONFIG.mail_enabled() && new_device {
|
||||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &device.updated_at, &device.name) {
|
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &device.updated_at, &device.name) {
|
||||||
@@ -197,6 +197,7 @@ fn twofactor_auth(
|
|||||||
user_uuid: &str,
|
user_uuid: &str,
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
|
ip: &ClientIp,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
||||||
@@ -216,8 +217,7 @@ fn twofactor_auth(
|
|||||||
|
|
||||||
let selected_twofactor = twofactors
|
let selected_twofactor = twofactors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|tf| tf.atype == selected_id && tf.enabled)
|
.find(|tf| tf.atype == selected_id && tf.enabled);
|
||||||
.nth(0);
|
|
||||||
|
|
||||||
use crate::api::core::two_factor as _tf;
|
use crate::api::core::two_factor as _tf;
|
||||||
use crate::crypto::ct_eq;
|
use crate::crypto::ct_eq;
|
||||||
@@ -226,7 +226,7 @@ fn twofactor_auth(
|
|||||||
let mut remember = data.two_factor_remember.unwrap_or(0);
|
let mut remember = data.two_factor_remember.unwrap_or(0);
|
||||||
|
|
||||||
match TwoFactorType::from_i32(selected_id) {
|
match TwoFactorType::from_i32(selected_id) {
|
||||||
Some(TwoFactorType::Authenticator) => _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, conn)?,
|
Some(TwoFactorType::Authenticator) => _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn)?,
|
||||||
Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
||||||
Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
||||||
Some(TwoFactorType::Duo) => _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
|
Some(TwoFactorType::Duo) => _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
mod admin;
|
mod admin;
|
||||||
pub(crate) mod core;
|
pub mod core;
|
||||||
mod icons;
|
mod icons;
|
||||||
mod identity;
|
mod identity;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
|
@@ -159,11 +159,12 @@ impl Handler for WSHandler {
|
|||||||
|
|
||||||
let (_id, access_token) = match path.split('?').nth(1) {
|
let (_id, access_token) = match path.split('?').nth(1) {
|
||||||
Some(params) => {
|
Some(params) => {
|
||||||
let mut params_iter = params.split('&').take(2);
|
let params_iter = params.split('&').take(2);
|
||||||
|
|
||||||
let mut id = None;
|
let mut id = None;
|
||||||
let mut access_token = None;
|
let mut access_token = None;
|
||||||
while let Some(val) = params_iter.next() {
|
|
||||||
|
for val in params_iter {
|
||||||
if val.starts_with(ID_KEY) {
|
if val.starts_with(ID_KEY) {
|
||||||
id = Some(&val[ID_KEY.len()..]);
|
id = Some(&val[ID_KEY.len()..]);
|
||||||
} else if val.starts_with(ACCESS_TOKEN_KEY) {
|
} else if val.starts_with(ACCESS_TOKEN_KEY) {
|
||||||
@@ -260,7 +261,9 @@ impl Factory for WSFactory {
|
|||||||
// Remove handler
|
// Remove handler
|
||||||
if let Some(user_uuid) = &handler.user_uuid {
|
if let Some(user_uuid) = &handler.user_uuid {
|
||||||
if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) {
|
if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) {
|
||||||
user_conn.remove_item(&handler.out);
|
if let Some(pos) = user_conn.iter().position(|x| x == &handler.out) {
|
||||||
|
user_conn.remove(pos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -78,6 +78,7 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> {
|
|||||||
match filename.as_ref() {
|
match filename.as_ref() {
|
||||||
"mail-github.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
|
"mail-github.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
|
||||||
"logo-gray.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
|
"logo-gray.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
|
||||||
|
"shield-white.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/shield-white.png"))),
|
||||||
"error-x.svg" => Ok(Content(ContentType::SVG, include_bytes!("../static/images/error-x.svg"))),
|
"error-x.svg" => Ok(Content(ContentType::SVG, include_bytes!("../static/images/error-x.svg"))),
|
||||||
"hibp.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/hibp.png"))),
|
"hibp.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/hibp.png"))),
|
||||||
|
|
||||||
|
@@ -112,6 +112,8 @@ macro_rules! make_config {
|
|||||||
)+)+
|
)+)+
|
||||||
config.domain_set = _domain_set;
|
config.domain_set = _domain_set;
|
||||||
|
|
||||||
|
config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase();
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,7 +135,6 @@ macro_rules! make_config {
|
|||||||
(inner._env.build(), inner.config.clone())
|
(inner._env.build(), inner.config.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
fn _get_form_type(rust_type: &str) -> &'static str {
|
fn _get_form_type(rust_type: &str) -> &'static str {
|
||||||
match rust_type {
|
match rust_type {
|
||||||
"Pass" => "password",
|
"Pass" => "password",
|
||||||
@@ -263,7 +264,7 @@ make_config! {
|
|||||||
/// $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.
|
||||||
disable_icon_download: bool, true, def, false;
|
disable_icon_download: bool, true, def, false;
|
||||||
/// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
|
/// Allow new signups |> Controls whether new users can register. Users can be invited by the bitwarden_rs admin even if this is disabled
|
||||||
signups_allowed: bool, true, def, true;
|
signups_allowed: bool, true, def, true;
|
||||||
/// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
|
/// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
|
||||||
signups_verify: bool, true, def, false;
|
signups_verify: bool, true, def, false;
|
||||||
@@ -271,9 +272,9 @@ make_config! {
|
|||||||
signups_verify_resend_time: u64, true, def, 3_600;
|
signups_verify_resend_time: u64, true, def, 3_600;
|
||||||
/// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)
|
/// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)
|
||||||
signups_verify_resend_limit: u32, true, def, 6;
|
signups_verify_resend_limit: u32, true, def, 6;
|
||||||
/// Allow signups only from this list of comma-separated domains
|
/// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled
|
||||||
signups_domains_whitelist: String, true, def, "".to_string();
|
signups_domains_whitelist: String, true, def, "".to_string();
|
||||||
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
|
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are otherwise disabled
|
||||||
invitations_allowed: bool, true, def, true;
|
invitations_allowed: bool, true, def, true;
|
||||||
/// Password iterations |> Number of server-side passwords hashing iterations.
|
/// Password iterations |> Number of server-side passwords hashing iterations.
|
||||||
/// The changes only apply when a user changes their password. Not recommended to lower the value
|
/// The changes only apply when a user changes their password. Not recommended to lower the value
|
||||||
@@ -428,6 +429,11 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
err!("DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'");
|
err!("DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let whitelist = &cfg.signups_domains_whitelist;
|
||||||
|
if !whitelist.is_empty() && whitelist.split(',').any(|d| d.trim().is_empty()) {
|
||||||
|
err!("`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens");
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(ref token) = cfg.admin_token {
|
if let Some(ref token) = cfg.admin_token {
|
||||||
if token.trim().is_empty() && !cfg.disable_admin_token {
|
if token.trim().is_empty() && !cfg.disable_admin_token {
|
||||||
println!("[WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled.");
|
println!("[WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled.");
|
||||||
@@ -552,18 +558,30 @@ impl Config {
|
|||||||
self.update_config(builder)
|
self.update_config(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_signup_user(&self, email: &str) -> bool {
|
/// Tests whether an email's domain is allowed. A domain is allowed if it
|
||||||
|
/// is in signups_domains_whitelist, or if no whitelist is set (so there
|
||||||
|
/// are no domain restrictions in effect).
|
||||||
|
pub fn is_email_domain_allowed(&self, email: &str) -> bool {
|
||||||
let e: Vec<&str> = email.rsplitn(2, '@').collect();
|
let e: Vec<&str> = email.rsplitn(2, '@').collect();
|
||||||
if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {
|
if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {
|
||||||
warn!("Failed to parse email address '{}'", email);
|
warn!("Failed to parse email address '{}'", email);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
let email_domain = e[0].to_lowercase();
|
||||||
|
let whitelist = self.signups_domains_whitelist();
|
||||||
|
|
||||||
// Allow signups if the whitelist is empty/not configured
|
whitelist.is_empty() || whitelist.split(',').any(|d| d.trim() == email_domain)
|
||||||
// (it doesn't contain any domains), or if it matches at least
|
}
|
||||||
// one domain.
|
|
||||||
let whitelist_str = self.signups_domains_whitelist();
|
/// Tests whether signup is allowed for an email address, taking into
|
||||||
( whitelist_str.is_empty() && CONFIG.signups_allowed() )|| whitelist_str.split(',').filter(|s| !s.is_empty()).any(|d| d == e[0])
|
/// account the signups_allowed and signups_domains_whitelist settings.
|
||||||
|
pub fn is_signup_allowed(&self, email: &str) -> bool {
|
||||||
|
if !self.signups_domains_whitelist().is_empty() {
|
||||||
|
// The whitelist setting overrides the signups_allowed setting.
|
||||||
|
self.is_email_domain_allowed(email)
|
||||||
|
} else {
|
||||||
|
self.signups_allowed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_user_config(&self) -> Result<(), Error> {
|
pub fn delete_user_config(&self) -> Result<(), Error> {
|
||||||
@@ -622,7 +640,7 @@ impl Config {
|
|||||||
pub fn is_admin_token_set(&self) -> bool {
|
pub fn is_admin_token_set(&self) -> bool {
|
||||||
let token = self.admin_token();
|
let token = self.admin_token();
|
||||||
|
|
||||||
!token.is_none() && !token.unwrap().trim().is_empty()
|
token.is_some() && !token.unwrap().trim().is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_template<T: serde::ser::Serialize>(
|
pub fn render_template<T: serde::ser::Serialize>(
|
||||||
@@ -682,7 +700,10 @@ where
|
|||||||
|
|
||||||
reg!("admin/base");
|
reg!("admin/base");
|
||||||
reg!("admin/login");
|
reg!("admin/login");
|
||||||
reg!("admin/page");
|
reg!("admin/settings");
|
||||||
|
reg!("admin/users");
|
||||||
|
reg!("admin/organizations");
|
||||||
|
reg!("admin/diagnostics");
|
||||||
|
|
||||||
// And then load user templates to overwrite the defaults
|
// And then load user templates to overwrite the defaults
|
||||||
// Use .hbs extension for the files
|
// Use .hbs extension for the files
|
||||||
|
@@ -5,6 +5,7 @@ use crate::CONFIG;
|
|||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "attachments"]
|
#[table_name = "attachments"]
|
||||||
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
||||||
#[primary_key(id)]
|
#[primary_key(id)]
|
||||||
pub struct Attachment {
|
pub struct Attachment {
|
||||||
@@ -17,7 +18,7 @@ pub struct Attachment {
|
|||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl Attachment {
|
impl Attachment {
|
||||||
pub fn new(id: String, cipher_uuid: String, file_name: String, file_size: i32) -> Self {
|
pub const fn new(id: String, cipher_uuid: String, file_name: String, file_size: i32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
cipher_uuid,
|
cipher_uuid,
|
||||||
@@ -51,7 +52,6 @@ impl Attachment {
|
|||||||
|
|
||||||
use crate::db::schema::{attachments, ciphers};
|
use crate::db::schema::{attachments, ciphers};
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
|
@@ -7,6 +7,7 @@ use super::{
|
|||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "ciphers"]
|
#[table_name = "ciphers"]
|
||||||
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[belongs_to(Organization, foreign_key = "organization_uuid")]
|
#[belongs_to(Organization, foreign_key = "organization_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
@@ -33,6 +34,7 @@ pub struct Cipher {
|
|||||||
|
|
||||||
pub favorite: bool,
|
pub favorite: bool,
|
||||||
pub password_history: Option<String>,
|
pub password_history: Option<String>,
|
||||||
|
pub deleted_at: Option<NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
@@ -57,13 +59,13 @@ impl Cipher {
|
|||||||
|
|
||||||
data: String::new(),
|
data: String::new(),
|
||||||
password_history: None,
|
password_history: None,
|
||||||
|
deleted_at: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::db::schema::*;
|
use crate::db::schema::*;
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
@@ -107,6 +109,7 @@ impl Cipher {
|
|||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"Type": self.atype,
|
"Type": self.atype,
|
||||||
"RevisionDate": format_date(&self.updated_at),
|
"RevisionDate": format_date(&self.updated_at),
|
||||||
|
"DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
|
||||||
"FolderId": self.get_folder_uuid(&user_uuid, &conn),
|
"FolderId": self.get_folder_uuid(&user_uuid, &conn),
|
||||||
"Favorite": self.favorite,
|
"Favorite": self.favorite,
|
||||||
"OrganizationId": self.organization_uuid,
|
"OrganizationId": self.organization_uuid,
|
||||||
@@ -352,6 +355,14 @@ impl Cipher {
|
|||||||
.load::<Self>(&**conn).expect("Error loading ciphers")
|
.load::<Self>(&**conn).expect("Error loading ciphers")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn count_owned_by_user(user_uuid: &str, conn: &DbConn) -> Option<i64> {
|
||||||
|
ciphers::table
|
||||||
|
.filter(ciphers::user_uuid.eq(user_uuid))
|
||||||
|
.count()
|
||||||
|
.first::<i64>(&**conn)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
ciphers::table
|
ciphers::table
|
||||||
.filter(ciphers::organization_uuid.eq(org_uuid))
|
.filter(ciphers::organization_uuid.eq(org_uuid))
|
||||||
|
@@ -35,7 +35,6 @@ impl Collection {
|
|||||||
|
|
||||||
use crate::db::schema::*;
|
use crate::db::schema::*;
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
|
@@ -5,6 +5,7 @@ use crate::CONFIG;
|
|||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "devices"]
|
#[table_name = "devices"]
|
||||||
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
pub struct Device {
|
pub struct Device {
|
||||||
@@ -76,7 +77,6 @@ impl Device {
|
|||||||
let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();
|
let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();
|
||||||
let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
||||||
|
|
||||||
|
|
||||||
// Create the JWT claims struct, to send to the client
|
// Create the JWT claims struct, to send to the client
|
||||||
use crate::auth::{encode_jwt, LoginJWTClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
|
use crate::auth::{encode_jwt, LoginJWTClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
|
||||||
let claims = LoginJWTClaims {
|
let claims = LoginJWTClaims {
|
||||||
@@ -107,7 +107,6 @@ impl Device {
|
|||||||
|
|
||||||
use crate::db::schema::devices;
|
use crate::db::schema::devices;
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
|
@@ -63,7 +63,6 @@ impl FolderCipher {
|
|||||||
|
|
||||||
use crate::db::schema::{folders, folders_ciphers};
|
use crate::db::schema::{folders, folders_ciphers};
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ pub struct OrgPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(FromPrimitive)]
|
#[derive(num_derive::FromPrimitive)]
|
||||||
pub enum OrgPolicyType {
|
pub enum OrgPolicyType {
|
||||||
TwoFactorAuthentication = 0,
|
TwoFactorAuthentication = 0,
|
||||||
MasterPassword = 1,
|
MasterPassword = 1,
|
||||||
|
@@ -34,7 +34,7 @@ pub enum UserOrgStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
#[derive(FromPrimitive)]
|
#[derive(num_derive::FromPrimitive)]
|
||||||
pub enum UserOrgType {
|
pub enum UserOrgType {
|
||||||
Owner = 0,
|
Owner = 0,
|
||||||
Admin = 1,
|
Admin = 1,
|
||||||
@@ -165,9 +165,9 @@ impl Organization {
|
|||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
|
|
||||||
"BusinessName": null,
|
"BusinessName": null,
|
||||||
"BusinessAddress1": null,
|
"BusinessAddress1": null,
|
||||||
"BusinessAddress2": null,
|
"BusinessAddress2": null,
|
||||||
"BusinessAddress3": null,
|
"BusinessAddress3": null,
|
||||||
"BusinessCountry": null,
|
"BusinessCountry": null,
|
||||||
"BusinessTaxNumber": null,
|
"BusinessTaxNumber": null,
|
||||||
|
|
||||||
@@ -198,7 +198,6 @@ impl UserOrganization {
|
|||||||
|
|
||||||
use crate::db::schema::{ciphers_collections, organizations, users_collections, users_organizations};
|
use crate::db::schema::{ciphers_collections, organizations, users_collections, users_organizations};
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
@@ -256,6 +255,10 @@ impl Organization {
|
|||||||
.first::<Self>(&**conn)
|
.first::<Self>(&**conn)
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_all(conn: &DbConn) -> Vec<Self> {
|
||||||
|
organizations::table.load::<Self>(&**conn).expect("Error loading organizations")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserOrganization {
|
impl UserOrganization {
|
||||||
@@ -275,6 +278,8 @@ impl UserOrganization {
|
|||||||
"UseGroups": false,
|
"UseGroups": false,
|
||||||
"UseTotp": true,
|
"UseTotp": true,
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
|
"UseApi": false,
|
||||||
|
"SelfHost": true,
|
||||||
|
|
||||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||||
|
|
||||||
@@ -305,7 +310,7 @@ impl UserOrganization {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json_collection_user_details(&self, read_only: bool) -> Value {
|
pub fn to_json_read_only(&self, read_only: bool) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"ReadOnly": read_only
|
"ReadOnly": read_only
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ pub struct TwoFactor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(FromPrimitive)]
|
#[derive(num_derive::FromPrimitive)]
|
||||||
pub enum TwoFactorType {
|
pub enum TwoFactorType {
|
||||||
Authenticator = 0,
|
Authenticator = 0,
|
||||||
Email = 1,
|
Email = 1,
|
||||||
@@ -60,7 +59,7 @@ impl TwoFactor {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json_list(&self) -> Value {
|
pub fn to_json_provider(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"Enabled": self.enabled,
|
"Enabled": self.enabled,
|
||||||
"Type": self.atype,
|
"Type": self.atype,
|
||||||
|
@@ -6,6 +6,7 @@ use crate::CONFIG;
|
|||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[table_name = "users"]
|
#[table_name = "users"]
|
||||||
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub uuid: String,
|
pub uuid: String,
|
||||||
@@ -120,7 +121,6 @@ impl User {
|
|||||||
use super::{Cipher, Device, Folder, TwoFactor, UserOrgType, UserOrganization};
|
use super::{Cipher, Device, Folder, TwoFactor, UserOrgType, UserOrganization};
|
||||||
use crate::db::schema::{invitations, users};
|
use crate::db::schema::{invitations, users};
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
@@ -274,7 +274,7 @@ pub struct Invitation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Invitation {
|
impl Invitation {
|
||||||
pub fn new(email: String) -> Self {
|
pub const fn new(email: String) -> Self {
|
||||||
Self { email }
|
Self { email }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ table! {
|
|||||||
data -> Text,
|
data -> Text,
|
||||||
favorite -> Bool,
|
favorite -> Bool,
|
||||||
password_history -> Nullable<Text>,
|
password_history -> Nullable<Text>,
|
||||||
|
deleted_at -> Nullable<Datetime>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ table! {
|
|||||||
data -> Text,
|
data -> Text,
|
||||||
favorite -> Bool,
|
favorite -> Bool,
|
||||||
password_history -> Nullable<Text>,
|
password_history -> Nullable<Text>,
|
||||||
|
deleted_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ table! {
|
|||||||
data -> Text,
|
data -> Text,
|
||||||
favorite -> Bool,
|
favorite -> Bool,
|
||||||
password_history -> Nullable<Text>,
|
password_history -> Nullable<Text>,
|
||||||
|
deleted_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
src/error.rs
19
src/error.rs
@@ -7,7 +7,6 @@ macro_rules! make_error {
|
|||||||
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {
|
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {
|
||||||
const BAD_REQUEST: u16 = 400;
|
const BAD_REQUEST: u16 = 400;
|
||||||
|
|
||||||
#[derive(Display)]
|
|
||||||
pub enum ErrorKind { $($name( $ty )),+ }
|
pub enum ErrorKind { $($name( $ty )),+ }
|
||||||
pub struct Error { message: String, error: ErrorKind, error_code: u16 }
|
pub struct Error { message: String, error: ErrorKind, error_code: u16 }
|
||||||
|
|
||||||
@@ -46,9 +45,13 @@ use std::option::NoneError as NoneErr;
|
|||||||
use std::time::SystemTimeError as TimeErr;
|
use std::time::SystemTimeError as TimeErr;
|
||||||
use u2f::u2ferror::U2fError as U2fErr;
|
use u2f::u2ferror::U2fError as U2fErr;
|
||||||
use yubico::yubicoerror::YubicoError as YubiErr;
|
use yubico::yubicoerror::YubicoError as YubiErr;
|
||||||
use lettre::smtp::error::Error as LettreErr;
|
|
||||||
|
|
||||||
#[derive(Display, Serialize)]
|
use lettre::address::AddressError as AddrErr;
|
||||||
|
use lettre::error::Error as LettreErr;
|
||||||
|
use lettre::message::mime::FromStrError as FromStrErr;
|
||||||
|
use lettre::transport::smtp::error::Error as SmtpErr;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
pub struct Empty {}
|
pub struct Empty {}
|
||||||
|
|
||||||
// Error struct
|
// Error struct
|
||||||
@@ -74,7 +77,11 @@ make_error! {
|
|||||||
ReqError(ReqErr): _has_source, _api_error,
|
ReqError(ReqErr): _has_source, _api_error,
|
||||||
RegexError(RegexErr): _has_source, _api_error,
|
RegexError(RegexErr): _has_source, _api_error,
|
||||||
YubiError(YubiErr): _has_source, _api_error,
|
YubiError(YubiErr): _has_source, _api_error,
|
||||||
LetreErr(LettreErr): _has_source, _api_error,
|
|
||||||
|
LetreError(LettreErr): _has_source, _api_error,
|
||||||
|
AddressError(AddrErr): _has_source, _api_error,
|
||||||
|
SmtpError(SmtpErr): _has_source, _api_error,
|
||||||
|
FromStrError(FromStrErr): _has_source, _api_error,
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is implemented by hand because NoneError doesn't implement neither Display nor Error
|
// This is implemented by hand because NoneError doesn't implement neither Display nor Error
|
||||||
@@ -118,7 +125,7 @@ impl Error {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_code(mut self, code: u16) -> Self {
|
pub const fn with_code(mut self, code: u16) -> Self {
|
||||||
self.error_code = code;
|
self.error_code = code;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -146,7 +153,7 @@ impl<S> MapResult<S> for Option<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _has_source<T>(e: T) -> Option<T> {
|
const fn _has_source<T>(e: T) -> Option<T> {
|
||||||
Some(e)
|
Some(e)
|
||||||
}
|
}
|
||||||
fn _no_source<T, S>(_: T) -> Option<S> {
|
fn _no_source<T, S>(_: T) -> Option<S> {
|
||||||
|
120
src/mail.rs
120
src/mail.rs
@@ -1,13 +1,11 @@
|
|||||||
use lettre::smtp::authentication::Credentials;
|
use std::str::FromStr;
|
||||||
use lettre::smtp::authentication::Mechanism as SmtpAuthMechanism;
|
|
||||||
use lettre::smtp::ConnectionReuseParameters;
|
use lettre::message::{header, Mailbox, Message, MultiPart, SinglePart};
|
||||||
use lettre::{
|
use lettre::transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism};
|
||||||
builder::{EmailBuilder, MimeMultipartType, PartBuilder},
|
use lettre::{Address, SmtpTransport, Tls, TlsParameters, Transport};
|
||||||
ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport,
|
|
||||||
};
|
|
||||||
use native_tls::{Protocol, TlsConnector};
|
use native_tls::{Protocol, TlsConnector};
|
||||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||||
use quoted_printable::encode_to_str;
|
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::auth::{encode_jwt, generate_delete_claims, generate_invite_claims, generate_verify_email_claims};
|
use crate::auth::{encode_jwt, generate_delete_claims, generate_invite_claims, generate_verify_email_claims};
|
||||||
@@ -24,43 +22,40 @@ fn mailer() -> SmtpTransport {
|
|||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let params = ClientTlsParameters::new(host.clone(), tls);
|
let params = TlsParameters::new(host.clone(), tls);
|
||||||
|
|
||||||
if CONFIG.smtp_explicit_tls() {
|
if CONFIG.smtp_explicit_tls() {
|
||||||
ClientSecurity::Wrapper(params)
|
Tls::Wrapper(params)
|
||||||
} else {
|
} else {
|
||||||
ClientSecurity::Required(params)
|
Tls::Required(params)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ClientSecurity::None
|
Tls::None
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
let smtp_client = SmtpClient::new((host.as_str(), CONFIG.smtp_port()), client_security).unwrap();
|
let smtp_client = SmtpTransport::builder(host).port(CONFIG.smtp_port()).tls(client_security);
|
||||||
|
|
||||||
let smtp_client = match (&CONFIG.smtp_username(), &CONFIG.smtp_password()) {
|
let smtp_client = match (CONFIG.smtp_username(), CONFIG.smtp_password()) {
|
||||||
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user.clone(), pass.clone())),
|
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user, pass)),
|
||||||
_ => smtp_client,
|
_ => smtp_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
let smtp_client = match CONFIG.smtp_auth_mechanism() {
|
let smtp_client = match CONFIG.smtp_auth_mechanism() {
|
||||||
Some(mechanism) => {
|
Some(mechanism) => {
|
||||||
let correct_mechanism = format!("\"{}\"", crate::util::upcase_first(&mechanism.trim_matches('"')));
|
let correct_mechanism = format!("\"{}\"", crate::util::upcase_first(mechanism.trim_matches('"')));
|
||||||
|
|
||||||
|
// TODO: Allow more than one mechanism
|
||||||
match serde_json::from_str::<SmtpAuthMechanism>(&correct_mechanism) {
|
match serde_json::from_str::<SmtpAuthMechanism>(&correct_mechanism) {
|
||||||
Ok(auth_mechanism) => smtp_client.authentication_mechanism(auth_mechanism),
|
Ok(auth_mechanism) => smtp_client.authentication(vec![auth_mechanism]),
|
||||||
_ => panic!("Failure to parse mechanism. Is it proper Json? Eg. `\"Plain\"` not `Plain`"),
|
_ => panic!("Failure to parse mechanism. Is it proper Json? Eg. `\"Plain\"` not `Plain`"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => smtp_client,
|
_ => smtp_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
smtp_client
|
smtp_client.timeout(Some(Duration::from_secs(CONFIG.smtp_timeout()))).build()
|
||||||
.smtp_utf8(true)
|
|
||||||
.timeout(Some(Duration::from_secs(CONFIG.smtp_timeout())))
|
|
||||||
.connection_reuse(ConnectionReuseParameters::NoReuse)
|
|
||||||
.transport()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> {
|
fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> {
|
||||||
@@ -95,7 +90,7 @@ pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
|
|||||||
|
|
||||||
let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?;
|
let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
||||||
@@ -112,7 +107,7 @@ pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
||||||
@@ -129,7 +124,7 @@ pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_welcome(address: &str) -> EmptyResult {
|
pub fn send_welcome(address: &str) -> EmptyResult {
|
||||||
@@ -140,7 +135,7 @@ pub fn send_welcome(address: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
||||||
@@ -156,7 +151,7 @@ pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invite(
|
pub fn send_invite(
|
||||||
@@ -188,7 +183,7 @@ pub fn send_invite(
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
|
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
|
||||||
@@ -201,7 +196,7 @@ pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str)
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
||||||
@@ -213,7 +208,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: &NaiveDateTime, device: &str) -> EmptyResult {
|
pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult {
|
||||||
@@ -232,7 +227,7 @@ pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, de
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_token(address: &str, token: &str) -> EmptyResult {
|
pub fn send_token(address: &str, token: &str) -> EmptyResult {
|
||||||
@@ -244,7 +239,7 @@ pub fn send_token(address: &str, token: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
||||||
@@ -256,7 +251,7 @@ pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_test(address: &str) -> EmptyResult {
|
pub fn send_test(address: &str) -> EmptyResult {
|
||||||
@@ -267,7 +262,7 @@ pub fn send_test(address: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(&address, &subject, &body_html, &body_text)
|
send_email(address, &subject, &body_html, &body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult {
|
fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult {
|
||||||
@@ -283,38 +278,35 @@ fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) ->
|
|||||||
|
|
||||||
let address = format!("{}@{}", address_split[1], domain_puny);
|
let address = format!("{}@{}", address_split[1], domain_puny);
|
||||||
|
|
||||||
let html = PartBuilder::new()
|
let data = MultiPart::mixed()
|
||||||
.body(encode_to_str(body_html))
|
.multipart(
|
||||||
.header(("Content-Type", "text/html; charset=utf-8"))
|
MultiPart::alternative()
|
||||||
.header(("Content-Transfer-Encoding", "quoted-printable"))
|
.singlepart(
|
||||||
.build();
|
SinglePart::quoted_printable()
|
||||||
|
.header(header::ContentType("text/plain; charset=utf-8".parse()?))
|
||||||
|
.body(body_text),
|
||||||
|
)
|
||||||
|
.multipart(
|
||||||
|
MultiPart::related().singlepart(
|
||||||
|
SinglePart::quoted_printable()
|
||||||
|
.header(header::ContentType("text/html; charset=utf-8".parse()?))
|
||||||
|
.body(body_html),
|
||||||
|
)
|
||||||
|
// .singlepart(SinglePart::base64() -- Inline files would go here
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// .singlepart(SinglePart::base64() -- Attachments would go here
|
||||||
|
;
|
||||||
|
|
||||||
let text = PartBuilder::new()
|
let email = Message::builder()
|
||||||
.body(encode_to_str(body_text))
|
.to(Mailbox::new(None, Address::from_str(&address)?))
|
||||||
.header(("Content-Type", "text/plain; charset=utf-8"))
|
.from(Mailbox::new(
|
||||||
.header(("Content-Transfer-Encoding", "quoted-printable"))
|
Some(CONFIG.smtp_from_name()),
|
||||||
.build();
|
Address::from_str(&CONFIG.smtp_from())?,
|
||||||
|
))
|
||||||
let alternative = PartBuilder::new()
|
|
||||||
.message_type(MimeMultipartType::Alternative)
|
|
||||||
.child(text)
|
|
||||||
.child(html);
|
|
||||||
|
|
||||||
let email = EmailBuilder::new()
|
|
||||||
.to(address)
|
|
||||||
.from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str()))
|
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.child(alternative.build())
|
.multipart(data)?;
|
||||||
.build()
|
|
||||||
.map_err(|e| Error::new("Error building email", e.to_string()))?;
|
|
||||||
|
|
||||||
let mut transport = mailer();
|
let _ = mailer().send(&email)?;
|
||||||
|
|
||||||
let result = transport.send(email);
|
|
||||||
|
|
||||||
// Explicitly close the connection, in case of error
|
|
||||||
transport.close();
|
|
||||||
|
|
||||||
result?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
15
src/main.rs
15
src/main.rs
@@ -1,7 +1,7 @@
|
|||||||
#![feature(proc_macro_hygiene, vec_remove_item, try_trait, ip)]
|
#![forbid(unsafe_code)]
|
||||||
|
#![feature(proc_macro_hygiene, try_trait, ip)]
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
extern crate openssl;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@@ -14,12 +14,6 @@ extern crate log;
|
|||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel_migrations;
|
extern crate diesel_migrations;
|
||||||
#[macro_use]
|
|
||||||
extern crate derive_more;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate num_derive;
|
|
||||||
|
|
||||||
extern crate backtrace;
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::create_dir_all,
|
fs::create_dir_all,
|
||||||
@@ -108,7 +102,10 @@ fn launch_info() {
|
|||||||
println!("|--------------------------------------------------------------------|");
|
println!("|--------------------------------------------------------------------|");
|
||||||
println!("| This is an *unofficial* Bitwarden implementation, DO NOT use the |");
|
println!("| This is an *unofficial* Bitwarden implementation, DO NOT use the |");
|
||||||
println!("| official channels to report bugs/features, regardless of client. |");
|
println!("| official channels to report bugs/features, regardless of client. |");
|
||||||
println!("| Report URL: https://github.com/dani-garcia/bitwarden_rs/issues/new |");
|
println!("| Send usage/configuration questions or feature requests to: |");
|
||||||
|
println!("| https://bitwardenrs.discourse.group/ |");
|
||||||
|
println!("| Report suspected bugs/issues in the software itself at: |");
|
||||||
|
println!("| https://github.com/dani-garcia/bitwarden_rs/issues/new |");
|
||||||
println!("\\--------------------------------------------------------------------/\n");
|
println!("\\--------------------------------------------------------------------/\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -108,7 +108,9 @@
|
|||||||
"microsoftonline.com",
|
"microsoftonline.com",
|
||||||
"office365.com",
|
"office365.com",
|
||||||
"microsoftstore.com",
|
"microsoftstore.com",
|
||||||
"xbox.com"
|
"xbox.com",
|
||||||
|
"azure.com",
|
||||||
|
"windowsazure.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
@@ -126,8 +128,7 @@
|
|||||||
"Type": 12,
|
"Type": 12,
|
||||||
"Domains": [
|
"Domains": [
|
||||||
"overture.com",
|
"overture.com",
|
||||||
"yahoo.com",
|
"yahoo.com"
|
||||||
"flickr.com"
|
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
@@ -192,7 +193,6 @@
|
|||||||
"amazon.it",
|
"amazon.it",
|
||||||
"amazon.com.au",
|
"amazon.com.au",
|
||||||
"amazon.co.nz",
|
"amazon.co.nz",
|
||||||
"amazon.co.jp",
|
|
||||||
"amazon.in"
|
"amazon.in"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
@@ -777,69 +777,69 @@
|
|||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Type": 76,
|
"Type": 76,
|
||||||
"Domains": [
|
"Domains": [
|
||||||
"docusign.com",
|
"docusign.com",
|
||||||
"docusign.net"
|
"docusign.net"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Type": 77,
|
"Type": 77,
|
||||||
"Domains": [
|
"Domains": [
|
||||||
"envato.com",
|
"envato.com",
|
||||||
"themeforest.net",
|
"themeforest.net",
|
||||||
"codecanyon.net",
|
"codecanyon.net",
|
||||||
"videohive.net",
|
"videohive.net",
|
||||||
"audiojungle.net",
|
"audiojungle.net",
|
||||||
"graphicriver.net",
|
"graphicriver.net",
|
||||||
"photodune.net",
|
"photodune.net",
|
||||||
"3docean.net"
|
"3docean.net"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Type": 78,
|
"Type": 78,
|
||||||
"Domains": [
|
"Domains": [
|
||||||
"x10hosting.com",
|
"x10hosting.com",
|
||||||
"x10premium.com"
|
"x10premium.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Type": 79,
|
"Type": 79,
|
||||||
"Domains": [
|
"Domains": [
|
||||||
"dnsomatic.com",
|
"dnsomatic.com",
|
||||||
"opendns.com",
|
"opendns.com",
|
||||||
"umbrella.com"
|
"umbrella.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Type": 80,
|
"Type": 80,
|
||||||
"Domains": [
|
"Domains": [
|
||||||
"cagreatamerica.com",
|
"cagreatamerica.com",
|
||||||
"canadaswonderland.com",
|
"canadaswonderland.com",
|
||||||
"carowinds.com",
|
"carowinds.com",
|
||||||
"cedarfair.com",
|
"cedarfair.com",
|
||||||
"cedarpoint.com",
|
"cedarpoint.com",
|
||||||
"dorneypark.com",
|
"dorneypark.com",
|
||||||
"kingsdominion.com",
|
"kingsdominion.com",
|
||||||
"knotts.com",
|
"knotts.com",
|
||||||
"miadventure.com",
|
"miadventure.com",
|
||||||
"schlitterbahn.com",
|
"schlitterbahn.com",
|
||||||
"valleyfair.com",
|
"valleyfair.com",
|
||||||
"visitkingsisland.com",
|
"visitkingsisland.com",
|
||||||
"worldsoffun.com"
|
"worldsoffun.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Type": 81,
|
"Type": 81,
|
||||||
"Domains": [
|
"Domains": [
|
||||||
"ubnt.com",
|
"ubnt.com",
|
||||||
"ui.com"
|
"ui.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 5.8 KiB |
BIN
src/static/images/shield-white.png
Normal file
BIN
src/static/images/shield-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
3439
src/static/scripts/bootstrap-native-v4.js
vendored
3439
src/static/scripts/bootstrap-native-v4.js
vendored
File diff suppressed because it is too large
Load Diff
746
src/static/scripts/bootstrap.css
vendored
746
src/static/scripts/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,67 +1,127 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
<title>Bitwarden_rs Admin Panel</title>
|
<title>Bitwarden_rs Admin Panel</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{urlpath}}/bwrs_static/bootstrap.css" />
|
<link rel="stylesheet" href="{{urlpath}}/bwrs_static/bootstrap.css" />
|
||||||
<script src="{{urlpath}}/bwrs_static/bootstrap-native-v4.js"></script>
|
|
||||||
<script src="{{urlpath}}/bwrs_static/md5.js"></script>
|
|
||||||
<script src="{{urlpath}}/bwrs_static/identicon.js"></script>
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
padding-top: 70px;
|
padding-top: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width:768px) {
|
|
||||||
body {
|
|
||||||
padding-top: 190px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
.navbar img {
|
||||||
|
height: 24px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="{{urlpath}}/bwrs_static/md5.js"></script>
|
||||||
|
<script src="{{urlpath}}/bwrs_static/identicon.js"></script>
|
||||||
|
<script>
|
||||||
|
function reload() { window.location.reload(); }
|
||||||
|
function msg(text, reload_page = true) {
|
||||||
|
text && alert(text);
|
||||||
|
reload_page && reload();
|
||||||
|
}
|
||||||
|
function identicon(email) {
|
||||||
|
const data = new Identicon(md5(email), { size: 48, format: 'svg' });
|
||||||
|
return "data:image/svg+xml;base64," + data.toString();
|
||||||
|
}
|
||||||
|
function toggleVis(input_id) {
|
||||||
|
const elem = document.getElementById(input_id);
|
||||||
|
const type = elem.getAttribute("type");
|
||||||
|
if (type === "text") {
|
||||||
|
elem.setAttribute("type", "password");
|
||||||
|
} else {
|
||||||
|
elem.setAttribute("type", "text");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function _post(url, successMsg, errMsg, body, reload_page = true) {
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: body,
|
||||||
|
mode: "same-origin",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
}).then( resp => {
|
||||||
|
if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); }
|
||||||
|
respStatus = resp.status;
|
||||||
|
respStatusText = resp.statusText;
|
||||||
|
return resp.text();
|
||||||
|
}).then( respText => {
|
||||||
|
try {
|
||||||
|
const respJson = JSON.parse(respText);
|
||||||
|
return respJson ? respJson.ErrorModel.Message : "Unknown error";
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject({body:respStatus + ' - ' + respStatusText, error: true});
|
||||||
|
}
|
||||||
|
}).then( apiMsg => {
|
||||||
|
msg(errMsg + "\n" + apiMsg, reload_page);
|
||||||
|
}).catch( e => {
|
||||||
|
if (e.error === false) { return true; }
|
||||||
|
else { msg(errMsg + "\n" + e.body, reload_page); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<nav class="navbar navbar-expand-sm navbar-dark bg-dark fixed-top shadow">
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
|
||||||
<a class="navbar-brand" href="#">Bitwarden_rs</a>
|
<div class="container">
|
||||||
<div class="navbar-collapse">
|
<a class="navbar-brand" href="{{urlpath}}/admin"><img class="pr-1" src="{{urlpath}}/bwrs_static/shield-white.png">Bitwarden_rs Admin</a>
|
||||||
<ul class="navbar-nav">
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse"
|
||||||
<li class="nav-item active">
|
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<a class="nav-link" href="{{urlpath}}/admin">Admin Panel</a>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</li>
|
</button>
|
||||||
<li class="nav-item">
|
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||||
<a class="nav-link" href="{{urlpath}}/">Vault</a>
|
<ul class="navbar-nav mr-auto">
|
||||||
</li>
|
{{#if logged_in}}
|
||||||
</ul>
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{urlpath}}/admin">Settings</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{urlpath}}/admin/users/overview">Users</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{urlpath}}/admin/organizations/overview">Organizations</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{urlpath}}/admin/diagnostics">Diagnostics</a>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{urlpath}}/">Vault</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{#if logged_in}}
|
||||||
|
<a class="btn btn-sm btn-secondary" href="{{urlpath}}/admin/logout">Log Out</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
{{#if version}}
|
|
||||||
<li class="nav-item">
|
|
||||||
<span class="navbar-text mr-2">Version: {{version}}</span>
|
|
||||||
</li>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if logged_in}}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{{urlpath}}/admin/logout">Log Out</a>
|
|
||||||
</li>
|
|
||||||
{{/if}}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{{> (page_content) }}
|
{{> (page_content) }}
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<!-- This script needs to be at the bottom, else it will fail! -->
|
||||||
|
<script>
|
||||||
|
// get current URL path and assign 'active' class to the correct nav-item
|
||||||
|
(function () {
|
||||||
|
var pathname = window.location.pathname;
|
||||||
|
if (pathname === "") return;
|
||||||
|
var navItem = document.querySelectorAll('.navbar-nav .nav-item a[href="'+pathname+'"]');
|
||||||
|
if (navItem.length === 1) {
|
||||||
|
navItem[0].parentElement.className = navItem[0].parentElement.className + ' active';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- This script needs to be at the bottom, else it will fail! -->
|
||||||
|
<script src="{{urlpath}}/bwrs_static/bootstrap-native-v4.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
117
src/static/templates/admin/diagnostics.hbs
Normal file
117
src/static/templates/admin/diagnostics.hbs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<main class="container">
|
||||||
|
<div id="diagnostics-block" class="my-3 p-3 bg-white rounded shadow">
|
||||||
|
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
|
||||||
|
|
||||||
|
<h3>Version</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-5">Server Installed
|
||||||
|
<span class="badge badge-success d-none" id="server-success" title="Latest version is installed.">Ok</span>
|
||||||
|
<span class="badge badge-warning d-none" id="server-warning" title="There seems to be an update available.">Update</span>
|
||||||
|
<span class="badge badge-danger d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span id="server-installed">{{version}}</span>
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-5">Server Latest</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span id="server-latest">{{diagnostics.latest_release}}<span id="server-latest-commit" class="d-none">-{{diagnostics.latest_commit}}</span></span>
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-5">Web Installed
|
||||||
|
<span class="badge badge-success d-none" id="web-success" title="Latest version is installed.">Ok</span>
|
||||||
|
<span class="badge badge-warning d-none" id="web-warning" title="There seems to be an update available.">Update</span>
|
||||||
|
<span class="badge badge-danger d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span id="web-installed">{{diagnostics.web_vault_version}}</span>
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-5">Web Latest</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span id="web-latest">{{diagnostics.latest_web_build}}</span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Checks</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-5">DNS (github.com)
|
||||||
|
<span class="badge badge-success d-none" id="dns-success" title="DNS Resolving works!">Ok</span>
|
||||||
|
<span class="badge badge-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span id="dns-resolved">{{diagnostics.dns_resolved}}</span>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-5">Date & Time (UTC)
|
||||||
|
<span class="badge badge-success d-none" id="time-success" title="Time offsets seem to be correct.">Ok</span>
|
||||||
|
<span class="badge badge-danger d-none" id="time-warning" title="Time offsets are too mouch at drift.">Error</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{diagnostics.server_time}}</span></span>
|
||||||
|
<span id="time-browser" class="d-block"><b>Browser:</b> <span id="time-browser-string"></span></span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const d = new Date();
|
||||||
|
const year = d.getUTCFullYear();
|
||||||
|
const month = String((d.getUTCMonth()+1)).padStart(2, '0');
|
||||||
|
const day = String(d.getUTCDate()).padStart(2, '0');
|
||||||
|
const hour = String(d.getUTCHours()).padStart(2, '0');
|
||||||
|
const minute = String(d.getUTCMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(d.getUTCSeconds()).padStart(2, '0');
|
||||||
|
const browserUTC = year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + seconds;
|
||||||
|
document.getElementById("time-browser-string").innerText = browserUTC;
|
||||||
|
|
||||||
|
const serverUTC = document.getElementById("time-server-string").innerText;
|
||||||
|
const timeDrift = (Date.parse(serverUTC) - Date.parse(browserUTC)) / 1000;
|
||||||
|
if (timeDrift > 30 || timeDrift < -30) {
|
||||||
|
document.getElementById('time-warning').classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
document.getElementById('time-success').classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the output is a valid IP
|
||||||
|
const isValidIp = value => (/^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/.test(value) ? true : false);
|
||||||
|
if (isValidIp(document.getElementById('dns-resolved').innerText)) {
|
||||||
|
document.getElementById('dns-success').classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
document.getElementById('dns-warning').classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
let serverInstalled = document.getElementById('server-installed').innerText;
|
||||||
|
let serverLatest = document.getElementById('server-latest').innerText;
|
||||||
|
if (serverInstalled.indexOf('-') > -1 && serverLatest !== '-') {
|
||||||
|
document.getElementById('server-latest-commit').classList.remove('d-none');
|
||||||
|
serverLatest += document.getElementById('server-latest-commit').innerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webInstalled = document.getElementById('web-installed').innerText;
|
||||||
|
const webLatest = document.getElementById('web-latest').innerText;
|
||||||
|
|
||||||
|
checkVersions('server', serverInstalled, serverLatest);
|
||||||
|
checkVersions('web', webInstalled, webLatest);
|
||||||
|
|
||||||
|
function checkVersions(platform, installed, latest) {
|
||||||
|
if (installed === '-' || latest === '-') {
|
||||||
|
document.getElementById(platform + '-failed').classList.remove('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installed !== latest) {
|
||||||
|
document.getElementById(platform + '-warning').classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
document.getElementById(platform + '-success').classList.remove('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
30
src/static/templates/admin/organizations.hbs
Normal file
30
src/static/templates/admin/organizations.hbs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<main class="container">
|
||||||
|
<div id="organizations-block" class="my-3 p-3 bg-white rounded shadow">
|
||||||
|
<h6 class="border-bottom pb-2 mb-0">Organizations</h6>
|
||||||
|
|
||||||
|
<div id="organizations-list">
|
||||||
|
{{#each organizations}}
|
||||||
|
<div class="media pt-3">
|
||||||
|
<img class="mr-2 rounded identicon" data-src="{{Name}}_{{BillingEmail}}">
|
||||||
|
<div class="media-body pb-3 mb-0 small border-bottom">
|
||||||
|
<div class="row justify-content-between">
|
||||||
|
<div class="col">
|
||||||
|
<strong>{{Name}}</strong>
|
||||||
|
{{#if Id}}
|
||||||
|
<span class="badge badge-success ml-2">{{Id}}</span>
|
||||||
|
{{/if}}
|
||||||
|
<span class="d-block">{{BillingEmail}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll("img.identicon").forEach(function (e, i) {
|
||||||
|
e.src = identicon(e.dataset.src);
|
||||||
|
});
|
||||||
|
</script>
|
@@ -1,68 +1,4 @@
|
|||||||
<main class="container">
|
<main class="container">
|
||||||
<div id="users-block" class="my-3 p-3 bg-white rounded shadow">
|
|
||||||
<h6 class="border-bottom pb-2 mb-0">Registered Users</h6>
|
|
||||||
|
|
||||||
<div id="users-list">
|
|
||||||
{{#each users}}
|
|
||||||
<div class="media pt-3">
|
|
||||||
<img class="mr-2 rounded identicon" data-src="{{Email}}">
|
|
||||||
<div class="media-body pb-3 mb-0 small border-bottom">
|
|
||||||
<div class="row justify-content-between">
|
|
||||||
<div class="col">
|
|
||||||
<strong>{{Name}}</strong>
|
|
||||||
{{#if TwoFactorEnabled}}
|
|
||||||
<span class="badge badge-success ml-2">2FA</span>
|
|
||||||
{{/if}}
|
|
||||||
{{#case _Status 1}}
|
|
||||||
<span class="badge badge-warning ml-2">Invited</span>
|
|
||||||
{{/case}}
|
|
||||||
<span class="d-block">{{Email}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<strong> Organizations: </strong>
|
|
||||||
<span class="d-block">
|
|
||||||
{{#each Organizations}}
|
|
||||||
<span class="badge badge-primary" data-orgtype="{{Type}}">{{Name}}</span>
|
|
||||||
{{/each}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style="flex: 0 0 300px; font-size: 90%; text-align: right; padding-right: 15px">
|
|
||||||
{{#if TwoFactorEnabled}}
|
|
||||||
<a class="mr-2" href="#" onclick='remove2fa({{jsesc Id}})'>Remove all 2FA</a>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<a class="mr-2" href="#" onclick='deauthUser({{jsesc Id}})'>Deauthorize sessions</a>
|
|
||||||
<a class="mr-2" href="#" onclick='deleteUser({{jsesc Id}}, {{jsesc Email}})'>Delete User</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<button type="button" class="btn btn-sm btn-link" onclick="updateRevisions();"
|
|
||||||
title="Force all clients to fetch new data next time they connect. Useful after restoring a backup to remove any stale data.">
|
|
||||||
Force clients to resync
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-primary float-right" onclick="reload();">Reload users</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="invite-form-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow">
|
|
||||||
<div>
|
|
||||||
<h6 class="mb-0 text-white">Invite User</h6>
|
|
||||||
<small>Email:</small>
|
|
||||||
|
|
||||||
<form class="form-inline" id="invite-form" onsubmit="inviteUser(); return false;">
|
|
||||||
<input type="email" class="form-control w-50 mr-2" id="email-invite" placeholder="Enter email">
|
|
||||||
<button type="submit" class="btn btn-primary">Invite</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow">
|
<div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="text-white mb-3">Configuration</h6>
|
<h6 class="text-white mb-3">Configuration</h6>
|
||||||
@@ -202,90 +138,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function reload() { window.location.reload(); }
|
|
||||||
function msg(text, reload_page = true) {
|
|
||||||
text && alert(text);
|
|
||||||
reload_page && reload();
|
|
||||||
}
|
|
||||||
function identicon(email) {
|
|
||||||
const data = new Identicon(md5(email), { size: 48, format: 'svg' });
|
|
||||||
return "data:image/svg+xml;base64," + data.toString();
|
|
||||||
}
|
|
||||||
function toggleVis(input_id) {
|
|
||||||
const elem = document.getElementById(input_id);
|
|
||||||
const type = elem.getAttribute("type");
|
|
||||||
if (type === "text") {
|
|
||||||
elem.setAttribute("type", "password");
|
|
||||||
} else {
|
|
||||||
elem.setAttribute("type", "text");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function _post(url, successMsg, errMsg, body, reload_page = true) {
|
|
||||||
fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
body: body,
|
|
||||||
mode: "same-origin",
|
|
||||||
credentials: "same-origin",
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
}).then( resp => {
|
|
||||||
if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); }
|
|
||||||
respStatus = resp.status;
|
|
||||||
respStatusText = resp.statusText;
|
|
||||||
return resp.text();
|
|
||||||
}).then( respText => {
|
|
||||||
try {
|
|
||||||
const respJson = JSON.parse(respText);
|
|
||||||
return respJson ? respJson.ErrorModel.Message : "Unknown error";
|
|
||||||
} catch (e) {
|
|
||||||
return Promise.reject({body:respStatus + ' - ' + respStatusText, error: true});
|
|
||||||
}
|
|
||||||
}).then( apiMsg => {
|
|
||||||
msg(errMsg + "\n" + apiMsg, reload_page);
|
|
||||||
}).catch( e => {
|
|
||||||
if (e.error === false) { return true; }
|
|
||||||
else { msg(errMsg + "\n" + e.body, reload_page); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function deleteUser(id, mail) {
|
|
||||||
var input_mail = prompt("To delete user '" + mail + "', please type the email below")
|
|
||||||
if (input_mail != null) {
|
|
||||||
if (input_mail == mail) {
|
|
||||||
_post("{{urlpath}}/admin/users/" + id + "/delete",
|
|
||||||
"User deleted correctly",
|
|
||||||
"Error deleting user");
|
|
||||||
} else {
|
|
||||||
alert("Wrong email, please try again")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function remove2fa(id) {
|
|
||||||
_post("{{urlpath}}/admin/users/" + id + "/remove-2fa",
|
|
||||||
"2FA removed correctly",
|
|
||||||
"Error removing 2FA");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function deauthUser(id) {
|
|
||||||
_post("{{urlpath}}/admin/users/" + id + "/deauth",
|
|
||||||
"Sessions deauthorized correctly",
|
|
||||||
"Error deauthorizing sessions");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function updateRevisions() {
|
|
||||||
_post("{{urlpath}}/admin/users/update_revision",
|
|
||||||
"Success, clients will sync next time they connect",
|
|
||||||
"Error forcing clients to sync");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function inviteUser() {
|
|
||||||
inv = document.getElementById("email-invite");
|
|
||||||
data = JSON.stringify({ "email": inv.value });
|
|
||||||
inv.value = "";
|
|
||||||
_post("{{urlpath}}/admin/invite/", "User invited correctly",
|
|
||||||
"Error inviting user", data);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function smtpTest() {
|
function smtpTest() {
|
||||||
test_email = document.getElementById("smtp-test-email");
|
test_email = document.getElementById("smtp-test-email");
|
||||||
data = JSON.stringify({ "email": test_email.value });
|
data = JSON.stringify({ "email": test_email.value });
|
||||||
@@ -348,23 +200,6 @@
|
|||||||
onChange(); // Trigger the event initially
|
onChange(); // Trigger the event initially
|
||||||
checkbox.addEventListener("change", onChange);
|
checkbox.addEventListener("change", onChange);
|
||||||
}
|
}
|
||||||
let OrgTypes = {
|
|
||||||
"0": { "name": "Owner", "color": "orange" },
|
|
||||||
"1": { "name": "Admin", "color": "blueviolet" },
|
|
||||||
"2": { "name": "User", "color": "blue" },
|
|
||||||
"3": { "name": "Manager", "color": "green" },
|
|
||||||
};
|
|
||||||
|
|
||||||
document.querySelectorAll("img.identicon").forEach(function (e, i) {
|
|
||||||
e.src = identicon(e.dataset.src);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll("[data-orgtype]").forEach(function (e, i) {
|
|
||||||
let orgtype = OrgTypes[e.dataset.orgtype];
|
|
||||||
e.style.backgroundColor = orgtype.color;
|
|
||||||
e.title = orgtype.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
// These are formatted because otherwise the
|
// These are formatted because otherwise the
|
||||||
// VSCode formatter breaks But they still work
|
// VSCode formatter breaks But they still work
|
||||||
// {{#each config}} {{#if grouptoggle}}
|
// {{#each config}} {{#if grouptoggle}}
|
138
src/static/templates/admin/users.hbs
Normal file
138
src/static/templates/admin/users.hbs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<main class="container">
|
||||||
|
<div id="users-block" class="my-3 p-3 bg-white rounded shadow">
|
||||||
|
<h6 class="border-bottom pb-2 mb-0">Registered Users</h6>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="table-responsive-xl small">
|
||||||
|
<table class="table table-sm table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 24px;">User</th>
|
||||||
|
<th></th>
|
||||||
|
<th style="width:90px; min-width: 90px;">Items</th>
|
||||||
|
<th style="min-width: 140px;">Organizations</th>
|
||||||
|
<th style="width: 140px; min-width: 140px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each users}}
|
||||||
|
<tr>
|
||||||
|
<td><img class="mr-2 rounded identicon" data-src="{{Email}}"></td>
|
||||||
|
<td>
|
||||||
|
<strong>{{Name}}</strong>
|
||||||
|
<span class="d-block">{{Email}}</span>
|
||||||
|
<span class="d-block">
|
||||||
|
{{#if TwoFactorEnabled}}
|
||||||
|
<span class="badge badge-success mr-2" title="2FA is enabled">2FA</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#case _Status 1}}
|
||||||
|
<span class="badge badge-warning mr-2" title="User is invited">Invited</span>
|
||||||
|
{{/case}}
|
||||||
|
{{#if EmailVerified}}
|
||||||
|
<span class="badge badge-success mr-2" title="Email has been verified">Verified</span>
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="d-block">{{cipher_count}}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{#each Organizations}}
|
||||||
|
<span class="badge badge-primary" data-orgtype="{{Type}}">{{Name}}</span>
|
||||||
|
{{/each}}
|
||||||
|
</td>
|
||||||
|
<td style="font-size: 90%; text-align: right; padding-right: 15px">
|
||||||
|
{{#if TwoFactorEnabled}}
|
||||||
|
<a class="d-block" href="#" onclick='remove2fa({{jsesc Id}})'>Remove all 2FA</a>
|
||||||
|
{{/if}}
|
||||||
|
<a class="d-block" href="#" onclick='deauthUser({{jsesc Id}})'>Deauthorize sessions</a>
|
||||||
|
<a class="d-block" href="#" onclick='deleteUser({{jsesc Id}}, {{jsesc Email}})'>Delete User</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="updateRevisions();"
|
||||||
|
title="Force all clients to fetch new data next time they connect. Useful after restoring a backup to remove any stale data.">
|
||||||
|
Force clients to resync
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-primary float-right" onclick="reload();">Reload users</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="invite-form-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-0 text-white">Invite User</h6>
|
||||||
|
<small>Email:</small>
|
||||||
|
|
||||||
|
<form class="form-inline" id="invite-form" onsubmit="inviteUser(); return false;">
|
||||||
|
<input type="email" class="form-control w-50 mr-2" id="email-invite" placeholder="Enter email">
|
||||||
|
<button type="submit" class="btn btn-primary">Invite</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function deleteUser(id, mail) {
|
||||||
|
var input_mail = prompt("To delete user '" + mail + "', please type the email below")
|
||||||
|
if (input_mail != null) {
|
||||||
|
if (input_mail == mail) {
|
||||||
|
_post("{{urlpath}}/admin/users/" + id + "/delete",
|
||||||
|
"User deleted correctly",
|
||||||
|
"Error deleting user");
|
||||||
|
} else {
|
||||||
|
alert("Wrong email, please try again")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function remove2fa(id) {
|
||||||
|
_post("{{urlpath}}/admin/users/" + id + "/remove-2fa",
|
||||||
|
"2FA removed correctly",
|
||||||
|
"Error removing 2FA");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function deauthUser(id) {
|
||||||
|
_post("{{urlpath}}/admin/users/" + id + "/deauth",
|
||||||
|
"Sessions deauthorized correctly",
|
||||||
|
"Error deauthorizing sessions");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function updateRevisions() {
|
||||||
|
_post("{{urlpath}}/admin/users/update_revision",
|
||||||
|
"Success, clients will sync next time they connect",
|
||||||
|
"Error forcing clients to sync");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function inviteUser() {
|
||||||
|
inv = document.getElementById("email-invite");
|
||||||
|
data = JSON.stringify({ "email": inv.value });
|
||||||
|
inv.value = "";
|
||||||
|
_post("{{urlpath}}/admin/invite/", "User invited correctly",
|
||||||
|
"Error inviting user", data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let OrgTypes = {
|
||||||
|
"0": { "name": "Owner", "color": "orange" },
|
||||||
|
"1": { "name": "Admin", "color": "blueviolet" },
|
||||||
|
"2": { "name": "User", "color": "blue" },
|
||||||
|
"3": { "name": "Manager", "color": "green" },
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll("img.identicon").forEach(function (e, i) {
|
||||||
|
e.src = identicon(e.dataset.src);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-orgtype]").forEach(function (e, i) {
|
||||||
|
let orgtype = OrgTypes[e.dataset.orgtype];
|
||||||
|
e.style.backgroundColor = orgtype.color;
|
||||||
|
e.title = orgtype.name;
|
||||||
|
});
|
||||||
|
</script>
|
@@ -87,7 +87,7 @@ Your Email Change
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -87,7 +87,7 @@ Delete Your Account
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Invitation accepted
|
Invitation to {{{org_name}}} accepted
|
||||||
<!---------------->
|
<!---------------->
|
||||||
<html>
|
<html>
|
||||||
<p>
|
<p>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Invitation accepted
|
Invitation to {{{org_name}}} accepted
|
||||||
<!---------------->
|
<!---------------->
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<head>
|
<head>
|
||||||
@@ -87,7 +87,7 @@ Invitation accepted
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Invitation to {{org_name}} confirmed
|
Invitation to {{{org_name}}} confirmed
|
||||||
<!---------------->
|
<!---------------->
|
||||||
<html>
|
<html>
|
||||||
<p>
|
<p>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Invitation to {{org_name}} confirmed
|
Invitation to {{{org_name}}} confirmed
|
||||||
<!---------------->
|
<!---------------->
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<head>
|
<head>
|
||||||
@@ -87,7 +87,7 @@ Invitation to {{org_name}} confirmed
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
New Device Logged In From {{device}}
|
New Device Logged In From {{{device}}}
|
||||||
<!---------------->
|
<!---------------->
|
||||||
<html>
|
<html>
|
||||||
<p>
|
<p>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
New Device Logged In From {{device}}
|
New Device Logged In From {{{device}}}
|
||||||
<!---------------->
|
<!---------------->
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<head>
|
<head>
|
||||||
@@ -87,7 +87,7 @@ New Device Logged In From {{device}}
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -87,7 +87,7 @@ Sorry, you have no password hint...
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -87,7 +87,7 @@ Your master password hint
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Join {{org_name}}
|
Join {{{org_name}}}
|
||||||
<!---------------->
|
<!---------------->
|
||||||
<html>
|
<html>
|
||||||
<p>
|
<p>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Join {{org_name}}
|
Join {{{org_name}}}
|
||||||
<!---------------->
|
<!---------------->
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<head>
|
<head>
|
||||||
@@ -87,7 +87,7 @@ Join {{org_name}}
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -87,7 +87,7 @@ Bitwarden_rs SMTP Test
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -42,7 +42,7 @@ Your Two-step Login Verification Code
|
|||||||
body {
|
body {
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
}
|
}
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 410px) {
|
||||||
body {
|
body {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -86,10 +86,10 @@ Your Two-step Login Verification Code
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 410px !important; width: 410px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 410px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 410px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
<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;">
|
<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-wrap" 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: 20px; -webkit-text-size-adjust: none;" valign="top">
|
<td class="content-wrap" 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: 20px; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
@@ -87,7 +87,7 @@ Verify Your Email
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -87,7 +87,7 @@ Welcome
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
@@ -87,7 +87,7 @@ Welcome
|
|||||||
</tr>
|
</tr>
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;">
|
||||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||||
|
47
src/util.rs
47
src/util.rs
@@ -65,13 +65,13 @@ impl Fairing for CORS {
|
|||||||
let req_headers = request.headers();
|
let req_headers = request.headers();
|
||||||
|
|
||||||
// We need to explicitly get the Origin header for Access-Control-Allow-Origin
|
// We need to explicitly get the Origin header for Access-Control-Allow-Origin
|
||||||
let req_allow_origin = CORS::valid_url(CORS::get_header(&req_headers, "Origin"));
|
let req_allow_origin = CORS::valid_url(CORS::get_header(req_headers, "Origin"));
|
||||||
|
|
||||||
response.set_header(Header::new("Access-Control-Allow-Origin", req_allow_origin));
|
response.set_header(Header::new("Access-Control-Allow-Origin", req_allow_origin));
|
||||||
|
|
||||||
if request.method() == Method::Options {
|
if request.method() == Method::Options {
|
||||||
let req_allow_headers = CORS::get_header(&req_headers, "Access-Control-Request-Headers");
|
let req_allow_headers = CORS::get_header(req_headers, "Access-Control-Request-Headers");
|
||||||
let req_allow_method = CORS::get_header(&req_headers, "Access-Control-Request-Method");
|
let req_allow_method = CORS::get_header(req_headers, "Access-Control-Request-Method");
|
||||||
|
|
||||||
response.set_header(Header::new("Access-Control-Allow-Methods", req_allow_method));
|
response.set_header(Header::new("Access-Control-Allow-Methods", req_allow_method));
|
||||||
response.set_header(Header::new("Access-Control-Allow-Headers", req_allow_headers));
|
response.set_header(Header::new("Access-Control-Allow-Headers", req_allow_headers));
|
||||||
@@ -86,14 +86,14 @@ impl Fairing for CORS {
|
|||||||
pub struct Cached<R>(R, &'static str);
|
pub struct Cached<R>(R, &'static str);
|
||||||
|
|
||||||
impl<R> Cached<R> {
|
impl<R> Cached<R> {
|
||||||
pub fn long(r: R) -> Cached<R> {
|
pub const fn long(r: R) -> Cached<R> {
|
||||||
// 7 days
|
// 7 days
|
||||||
Cached(r, "public, max-age=604800")
|
Self(r, "public, max-age=604800")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn short(r: R) -> Cached<R> {
|
pub const fn short(r: R) -> Cached<R> {
|
||||||
// 10 minutes
|
// 10 minutes
|
||||||
Cached(r, "public, max-age=600")
|
Self(r, "public, max-age=600")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ impl Fairing for BetterLogging {
|
|||||||
let uri_subpath = request.uri().path().trim_start_matches(&CONFIG.domain_path());
|
let uri_subpath = request.uri().path().trim_start_matches(&CONFIG.domain_path());
|
||||||
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
|
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if let Some(ref route) = request.route() {
|
if let Some(route) = request.route() {
|
||||||
info!(target: "response", "{} => {} {}", route, status.code, status.reason)
|
info!(target: "response", "{} => {} {}", route, status.code, status.reason)
|
||||||
} else {
|
} else {
|
||||||
info!(target: "response", "{} {}", status.code, status.reason)
|
info!(target: "response", "{} {}", status.code, status.reason)
|
||||||
@@ -227,33 +227,6 @@ pub fn delete_file(path: &str) -> IOResult<()> {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LimitedReader<'a> {
|
|
||||||
reader: &'a mut dyn std::io::Read,
|
|
||||||
limit: usize, // In bytes
|
|
||||||
count: usize,
|
|
||||||
}
|
|
||||||
impl<'a> LimitedReader<'a> {
|
|
||||||
pub fn new(reader: &'a mut dyn std::io::Read, limit: usize) -> LimitedReader<'a> {
|
|
||||||
LimitedReader {
|
|
||||||
reader,
|
|
||||||
limit,
|
|
||||||
count: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> std::io::Read for LimitedReader<'a> {
|
|
||||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
|
||||||
self.count += buf.len();
|
|
||||||
|
|
||||||
if self.count > self.limit {
|
|
||||||
Ok(0) // End of the read
|
|
||||||
} else {
|
|
||||||
self.reader.read(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
|
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 {
|
||||||
@@ -269,9 +242,7 @@ pub fn get_display_size(size: i32) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Round to two decimals
|
format!("{:.2} {}", size, UNITS[unit_counter])
|
||||||
size = (size * 100.).round() / 100.;
|
|
||||||
format!("{} {}", size, UNITS[unit_counter])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_uuid() -> String {
|
pub fn get_uuid() -> String {
|
||||||
|
Reference in New Issue
Block a user