mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 10:45:57 +03:00
Compare commits
114 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
59e50b03bd | ||
|
0a88f020e1 | ||
|
c058a1d63c | ||
|
96a189deb9 | ||
|
8c229920ad | ||
|
d592323e39 | ||
|
95dd1cd7ad | ||
|
36ae946655 | ||
|
24edc94f9d | ||
|
4deae76347 | ||
|
8ee0c57224 | ||
|
cb6f392774 | ||
|
5c6081c4e2 | ||
|
88c56de97b | ||
|
e274af6e3d | ||
|
a0ece3754b | ||
|
0bcc2ae7ab | ||
|
bdb90460c4 | ||
|
824137a02c | ||
|
2edc699eac | ||
|
8e79366076 | ||
|
c1e39b182f | ||
|
13eb276085 | ||
|
4cec502f7b | ||
|
2545469713 | ||
|
f09996a21d | ||
|
5cabf4d040 | ||
|
a03db6d224 | ||
|
8d1b72b951 | ||
|
912e1f93b7 | ||
|
a5aa4d9b54 | ||
|
e777be3dde | ||
|
b5441f6b77 | ||
|
dbbd63e519 | ||
|
adc443ea80 | ||
|
0d32179d07 | ||
|
b45b02b37e | ||
|
12928b832c | ||
|
1e224220a8 | ||
|
3471e2660f | ||
|
924ba153aa | ||
|
bd1e8be328 | ||
|
cf5a985b31 | ||
|
607521c88f | ||
|
486c7d8c56 | ||
|
4b71197c97 | ||
|
8b8839d049 | ||
|
b209c1bc4d | ||
|
2b8d08a3f4 | ||
|
cbadf00941 | ||
|
c5b7447dac | ||
|
64d6f72e6c | ||
|
a19a6fb016 | ||
|
b889e5185e | ||
|
cd83a9e7b2 | ||
|
748c825202 | ||
|
204993568a | ||
|
70be2d93ce | ||
|
f5638716d2 | ||
|
fbc2fad9c9 | ||
|
3f39e35123 | ||
|
3f6809bcdf | ||
|
9ff577a7b4 | ||
|
c52adef919 | ||
|
cbb92bcbc0 | ||
|
948798a84f | ||
|
2ffc3eac4d | ||
|
0ff7fd939e | ||
|
ca7c5129b2 | ||
|
07e0fdbd2a | ||
|
b4dfc24040 | ||
|
85dbf4e16c | ||
|
efc65b93f8 | ||
|
9a0fe6f617 | ||
|
3442eb1b9d | ||
|
e449912f05 | ||
|
72a46fb386 | ||
|
d29b6bee28 | ||
|
e2e3712921 | ||
|
00a11b1b78 | ||
|
77b78f0991 | ||
|
ee550be80c | ||
|
97d41c2686 | ||
|
fccc0a4b05 | ||
|
57b1d3f850 | ||
|
77d40833d9 | ||
|
7814218208 | ||
|
95a7ffdf6b | ||
|
ebc47dc161 | ||
|
cd8acc2e8c | ||
|
3b7a5bd102 | ||
|
d3054d4f83 | ||
|
5ac66b05e3 | ||
|
83fd44eeef | ||
|
2edecf34ff | ||
|
18bc8331f9 | ||
|
7d956c5117 | ||
|
603a964579 | ||
|
dc515b83f3 | ||
|
9466f02696 | ||
|
d3bd2774dc | ||
|
f482585d7c | ||
|
2cde814aaa | ||
|
d989a19f76 | ||
|
d292269ea0 | ||
|
ebf40099f2 | ||
|
0586c00285 | ||
|
bb9ddd5680 | ||
|
cb1663fc12 | ||
|
45d9d8db94 | ||
|
edc482c8ea | ||
|
6e5c03cc78 | ||
|
881c1978eb | ||
|
662bc27523 |
@@ -21,6 +21,10 @@
|
|||||||
## Automatically reload the templates for every request, slow, use only for development
|
## Automatically reload the templates for every request, slow, use only for development
|
||||||
# RELOAD_TEMPLATES=false
|
# RELOAD_TEMPLATES=false
|
||||||
|
|
||||||
|
## Client IP Header, used to identify the IP of the client, defaults to "X-Client-IP"
|
||||||
|
## Set to the string "none" (without quotes), to disable any headers and just use the remote IP
|
||||||
|
# IP_HEADER=X-Client-IP
|
||||||
|
|
||||||
## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever")
|
## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever")
|
||||||
# ICON_CACHE_TTL=2592000
|
# ICON_CACHE_TTL=2592000
|
||||||
## Cache time-to-live for icons which weren't available, in seconds (0 is "forever")
|
## Cache time-to-live for icons which weren't available, in seconds (0 is "forever")
|
||||||
@@ -37,14 +41,10 @@
|
|||||||
# WEBSOCKET_ADDRESS=0.0.0.0
|
# WEBSOCKET_ADDRESS=0.0.0.0
|
||||||
# WEBSOCKET_PORT=3012
|
# WEBSOCKET_PORT=3012
|
||||||
|
|
||||||
## Enable extended logging
|
## Enable extended logging, which shows timestamps and targets in the logs
|
||||||
## This shows timestamps and allows logging to file and to syslog
|
|
||||||
### To enable logging to file, use the LOG_FILE env variable
|
|
||||||
### To enable syslog, use the USE_SYSLOG env variable
|
|
||||||
# EXTENDED_LOGGING=true
|
# EXTENDED_LOGGING=true
|
||||||
|
|
||||||
## Logging to file
|
## Logging to file
|
||||||
## This requires extended logging
|
|
||||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
||||||
# LOG_FILE=/path/to/log
|
# LOG_FILE=/path/to/log
|
||||||
|
|
||||||
@@ -56,7 +56,8 @@
|
|||||||
## Log level
|
## Log level
|
||||||
## Change the verbosity of the log output
|
## Change the verbosity of the log output
|
||||||
## Valid values are "trace", "debug", "info", "warn", "error" and "off"
|
## Valid values are "trace", "debug", "info", "warn", "error" and "off"
|
||||||
## This requires extended logging
|
## Setting it to "trace" or "debug" would also show logs for mounted
|
||||||
|
## routes and static file, websocket and alive requests
|
||||||
# LOG_LEVEL=Info
|
# LOG_LEVEL=Info
|
||||||
|
|
||||||
## Enable WAL for the DB
|
## Enable WAL for the DB
|
||||||
@@ -95,10 +96,31 @@
|
|||||||
## Controls if new users can register
|
## Controls if new users can register
|
||||||
# SIGNUPS_ALLOWED=true
|
# SIGNUPS_ALLOWED=true
|
||||||
|
|
||||||
|
## Controls if new users need to verify their email address upon registration
|
||||||
|
## Note that setting this option to true prevents logins until the email address has been verified!
|
||||||
|
## The welcome email will include a verification link, and login attempts will periodically
|
||||||
|
## trigger another verification email to be sent.
|
||||||
|
# SIGNUPS_VERIFY=false
|
||||||
|
|
||||||
|
## If SIGNUPS_VERIFY is set to true, this limits how many seconds after the last time
|
||||||
|
## an email verification link has been sent another verification email will be sent
|
||||||
|
# SIGNUPS_VERIFY_RESEND_TIME=3600
|
||||||
|
|
||||||
|
## If SIGNUPS_VERIFY is set to true, this limits how many times an email verification
|
||||||
|
## email will be re-sent upon an attempted login.
|
||||||
|
# SIGNUPS_VERIFY_RESEND_LIMIT=6
|
||||||
|
|
||||||
|
## Controls if new users from a list of comma-separated domains can register
|
||||||
|
## even if SIGNUPS_ALLOWED is set to false
|
||||||
|
# SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org
|
||||||
|
|
||||||
## Token for the admin interface, preferably use a long random string
|
## Token for the admin interface, preferably use a long random string
|
||||||
## One option is to use 'openssl rand -base64 48'
|
## One option is to use 'openssl rand -base64 48'
|
||||||
## If not set, the admin panel is disabled
|
## If not set, the admin panel is disabled
|
||||||
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
|
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
|
||||||
|
|
||||||
|
## Enable this to bypass the admin panel security. This option is only
|
||||||
|
## meant to be used with the use of a separate auth layer in front
|
||||||
# DISABLE_ADMIN_TOKEN=false
|
# DISABLE_ADMIN_TOKEN=false
|
||||||
|
|
||||||
## Invitations org admins to invite users, even when signups are disabled
|
## Invitations org admins to invite users, even when signups are disabled
|
||||||
@@ -137,6 +159,18 @@
|
|||||||
## After that, you should be able to follow the rest of the guide linked above,
|
## After that, you should be able to follow the rest of the guide linked above,
|
||||||
## ignoring the fields that ask for the values that you already configured beforehand.
|
## ignoring the fields that ask for the values that you already configured beforehand.
|
||||||
|
|
||||||
|
## Authenticator Settings
|
||||||
|
## Disable authenticator time drifted codes to be valid.
|
||||||
|
## TOTP codes of the previous and next 30 seconds will be invalid
|
||||||
|
##
|
||||||
|
## According to the RFC6238 (https://tools.ietf.org/html/rfc6238),
|
||||||
|
## we allow by default the TOTP code which was valid one step back and one in the future.
|
||||||
|
## This can however allow attackers to be a bit more lucky with there attempts because there are 3 valid codes.
|
||||||
|
## You can disable this, so that only the current TOTP Code is allowed.
|
||||||
|
## Keep in mind that when a sever drifts out of time, valid codes could be marked as invalid.
|
||||||
|
## In any case, if a code has been used it can not be used again, also codes which predates it will be invalid.
|
||||||
|
# AUTHENTICATOR_DISABLE_TIME_DRIFT = false
|
||||||
|
|
||||||
## Rocket specific settings, check Rocket documentation to learn more
|
## Rocket specific settings, check Rocket documentation to learn more
|
||||||
# ROCKET_ENV=staging
|
# ROCKET_ENV=staging
|
||||||
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
||||||
@@ -154,3 +188,6 @@
|
|||||||
# SMTP_USERNAME=username
|
# SMTP_USERNAME=username
|
||||||
# SMTP_PASSWORD=password
|
# SMTP_PASSWORD=password
|
||||||
# SMTP_AUTH_MECHANISM="Plain"
|
# SMTP_AUTH_MECHANISM="Plain"
|
||||||
|
# SMTP_TIMEOUT=15
|
||||||
|
|
||||||
|
# vim: syntax=ini
|
||||||
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: dani-garcia
|
33
.github/ISSUE_TEMPLATE.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!--
|
||||||
|
Please fill out the following template to make solving your problem easier and faster for us.
|
||||||
|
This is only a guideline. If you think that parts are unneccessary for your issue, feel free to remove them.
|
||||||
|
|
||||||
|
Remember to hide/obfuscate personal and confidential information,
|
||||||
|
such as names, global IP/DNS adresses and especially passwords, if neccessary.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Subject of the issue
|
||||||
|
<!-- Describe your issue here.-->
|
||||||
|
|
||||||
|
### Your environment
|
||||||
|
<!-- The version number, obtained from the logs or the admin page -->
|
||||||
|
* Bitwarden_rs version:
|
||||||
|
<!-- How the server was installed: Docker image / package / built from source -->
|
||||||
|
* Install method:
|
||||||
|
* Clients used: <!-- if applicable -->
|
||||||
|
* Reverse proxy and version: <!-- if applicable -->
|
||||||
|
* Version of mysql/postgresql: <!-- if applicable -->
|
||||||
|
* Other relevant information:
|
||||||
|
|
||||||
|
### Steps to reproduce
|
||||||
|
<!-- Tell us how to reproduce this issue. What parameters did you set (differently from the defaults)
|
||||||
|
and how did you start bitwarden_rs? -->
|
||||||
|
|
||||||
|
### Expected behaviour
|
||||||
|
<!-- Tell us what should happen -->
|
||||||
|
|
||||||
|
### Actual behaviour
|
||||||
|
<!-- Tell us what happens instead -->
|
||||||
|
|
||||||
|
### Relevant logs
|
||||||
|
<!-- Share some logfiles, screenshots or output of relevant programs with us. -->
|
70
.github/workflows/rust-win.yml.disabled
vendored
Normal file
70
.github/workflows/rust-win.yml.disabled
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: build-windows
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
db-backend: [sqlite, mysql, postgresql]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Cache choco cache
|
||||||
|
uses: actions/cache@v1.0.3
|
||||||
|
with:
|
||||||
|
path: ~\AppData\Local\Temp\chocolatey
|
||||||
|
key: ${{ runner.os }}-choco-cache
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: choco install openssl sqlite postgresql12 mysql
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v1.0.3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v1.0.3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: actions/cache@v1.0.3
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Install latest nightly
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo.exe build --verbose --features ${{ matrix.db-backend }} --release --target x86_64-pc-windows-msvc
|
||||||
|
env:
|
||||||
|
OPENSSL_DIR: C:\Program Files\OpenSSL-Win64\
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --features ${{ matrix.db-backend }}
|
||||||
|
|
||||||
|
- name: Upload windows artifact
|
||||||
|
uses: actions/upload-artifact@v1.0.0
|
||||||
|
with:
|
||||||
|
name: x86_64-pc-windows-msvc-${{ matrix.db-backend }}-bitwarden_rs
|
||||||
|
path: target/release/bitwarden_rs.exe
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: Shopify/upload-to-release@1.0.0
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
name: x86_64-pc-windows-msvc-${{ matrix.db-backend }}-bitwarden_rs
|
||||||
|
path: target/release/bitwarden_rs.exe
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
149
.github/workflows/workspace.yml
vendored
Normal file
149
.github/workflows/workspace.yml
vendored
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
name: Workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
db-backend: [sqlite, mysql, postgresql]
|
||||||
|
target:
|
||||||
|
- x86_64-unknown-linux-gnu
|
||||||
|
# - x86_64-unknown-linux-musl
|
||||||
|
- x86_64-apple-darwin
|
||||||
|
# - x86_64-pc-windows-msvc
|
||||||
|
include:
|
||||||
|
- target: x86_64-unknown-linux-gnu
|
||||||
|
os: ubuntu-latest
|
||||||
|
ext:
|
||||||
|
# - target: x86_64-unknown-linux-musl
|
||||||
|
# os: ubuntu-latest
|
||||||
|
# ext:
|
||||||
|
- target: x86_64-apple-darwin
|
||||||
|
os: macOS-latest
|
||||||
|
ext:
|
||||||
|
# - target: x86_64-pc-windows-msvc
|
||||||
|
# os: windows-latest
|
||||||
|
# ext: .exe
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
# - name: Cache choco cache
|
||||||
|
# uses: actions/cache@v1.0.3
|
||||||
|
# if: matrix.os == 'windows-latest'
|
||||||
|
# with:
|
||||||
|
# path: ~\AppData\Local\Temp\chocolatey
|
||||||
|
# key: ${{ runner.os }}-choco-cache-${{ matrix.db-backend }}
|
||||||
|
|
||||||
|
- name: Cache vcpkg installed
|
||||||
|
uses: actions/cache@v1.0.3
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
with:
|
||||||
|
path: $VCPKG_ROOT/installed
|
||||||
|
key: ${{ runner.os }}-vcpkg-cache-${{ matrix.db-backend }}
|
||||||
|
env:
|
||||||
|
VCPKG_ROOT: 'C:\vcpkg'
|
||||||
|
|
||||||
|
- name: Cache vcpkg downloads
|
||||||
|
uses: actions/cache@v1.0.3
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
with:
|
||||||
|
path: $VCPKG_ROOT/downloads
|
||||||
|
key: ${{ runner.os }}-vcpkg-cache-${{ matrix.db-backend }}
|
||||||
|
env:
|
||||||
|
VCPKG_ROOT: 'C:\vcpkg'
|
||||||
|
|
||||||
|
# - name: Cache homebrew
|
||||||
|
# uses: actions/cache@v1.0.3
|
||||||
|
# if: matrix.os == 'macOS-latest'
|
||||||
|
# with:
|
||||||
|
# path: ~/Library/Caches/Homebrew
|
||||||
|
# key: ${{ runner.os }}-brew-cache
|
||||||
|
|
||||||
|
# - name: Cache apt
|
||||||
|
# uses: actions/cache@v1.0.3
|
||||||
|
# if: matrix.os == 'ubuntu-latest'
|
||||||
|
# with:
|
||||||
|
# path: /var/cache/apt/archives
|
||||||
|
# key: ${{ runner.os }}-apt-cache
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
- name: Install dependencies macOS
|
||||||
|
run: brew update; brew install openssl sqlite libpq mysql
|
||||||
|
if: matrix.os == 'macOS-latest'
|
||||||
|
|
||||||
|
- name: Install dependencies Ubuntu
|
||||||
|
run: sudo apt-get update && sudo apt-get install --no-install-recommends openssl sqlite libpq-dev libmysql++-dev
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
|
||||||
|
- name: Install dependencies Windows
|
||||||
|
run: vcpkg integrate install; vcpkg install sqlite3:x64-windows openssl:x64-windows libpq:x64-windows libmysql:x64-windows
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
env:
|
||||||
|
VCPKG_ROOT: 'C:\vcpkg'
|
||||||
|
# End Install dependencies
|
||||||
|
|
||||||
|
# Install rust nightly toolchain
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v1.0.3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-${{matrix.db-backend}}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v1.0.3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-${{matrix.db-backend}}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: actions/cache@v1.0.3
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: ${{ runner.os }}-${{matrix.db-backend}}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Install latest nightly
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
|
# Build
|
||||||
|
- name: Build Win
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: cargo.exe build --features ${{ matrix.db-backend }} --release --target ${{ matrix.target }}
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: -Ctarget-feature=+crt-static
|
||||||
|
VCPKG_ROOT: 'C:\vcpkg'
|
||||||
|
|
||||||
|
- name: Build macOS / Ubuntu
|
||||||
|
if: matrix.os == 'macOS-latest' || matrix.os == 'ubuntu-latest'
|
||||||
|
run: cargo build --verbose --features ${{ matrix.db-backend }} --release --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
# Test
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --features ${{ matrix.db-backend }}
|
||||||
|
|
||||||
|
# Upload & Release
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v1.0.0
|
||||||
|
with:
|
||||||
|
name: bitwarden_rs-${{ matrix.db-backend }}-${{ matrix.target }}${{ matrix.ext }}
|
||||||
|
path: target/${{ matrix.target }}/release/bitwarden_rs${{ matrix.ext }}
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: Shopify/upload-to-release@1.0.0
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
name: bitwarden_rs-${{ matrix.db-backend }}-${{ matrix.target }}${{ matrix.ext }}
|
||||||
|
path: target/${{ matrix.target }}/release/bitwarden_rs${{ matrix.ext }}
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
@@ -11,10 +11,11 @@ cache: cargo
|
|||||||
before_install:
|
before_install:
|
||||||
- sudo curl -L https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VERSION/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint
|
- sudo curl -L https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VERSION/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint
|
||||||
- sudo chmod +rx /usr/local/bin/hadolint
|
- sudo chmod +rx /usr/local/bin/hadolint
|
||||||
|
- rustup set profile minimal
|
||||||
|
|
||||||
# Nothing to install
|
# Nothing to install
|
||||||
install: true
|
install: true
|
||||||
script:
|
script:
|
||||||
- git ls-files --exclude='Dockerfile*' --ignored | xargs --max-lines=1 hadolint
|
- git ls-files --exclude='Dockerfile*' --ignored | xargs --max-lines=1 hadolint
|
||||||
- cargo build --features "sqlite"
|
- cargo test --features "sqlite"
|
||||||
- cargo build --features "mysql"
|
- cargo test --features "mysql"
|
||||||
|
1204
Cargo.lock
generated
1204
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
51
Cargo.toml
@@ -26,44 +26,44 @@ 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 = "0.9.20"
|
reqwest = "0.9.24"
|
||||||
|
|
||||||
# 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 }
|
||||||
|
|
||||||
# WebSockets library
|
# WebSockets library
|
||||||
ws = "0.9.0"
|
ws = "0.9.1"
|
||||||
|
|
||||||
# MessagePack library
|
# MessagePack library
|
||||||
rmpv = "0.4.1"
|
rmpv = "0.4.3"
|
||||||
|
|
||||||
# Concurrent hashmap implementation
|
# Concurrent hashmap implementation
|
||||||
chashmap = "2.2.2"
|
chashmap = "2.2.2"
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = "1.0.101"
|
serde = "1.0.104"
|
||||||
serde_derive = "1.0.101"
|
serde_derive = "1.0.104"
|
||||||
serde_json = "1.0.40"
|
serde_json = "1.0.44"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
fern = { version = "0.5.8", features = ["syslog-4"] }
|
fern = { version = "0.5.9", features = ["syslog-4"] }
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "1.4.2", features = [ "chrono", "r2d2"] }
|
diesel = { version = "1.4.3", features = [ "chrono", "r2d2"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
|
|
||||||
# Bundled SQLite
|
# Bundled SQLite
|
||||||
libsqlite3-sys = { version = "0.12.0", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.16.0", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto library
|
# Crypto library
|
||||||
ring = "0.14.6"
|
ring = "0.14.6"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "0.7.4", features = ["v4"] }
|
uuid = { version = "0.8.1", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time library for Rust
|
# Date and time library for Rust
|
||||||
chrono = "0.4.9"
|
chrono = "0.4.10"
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
oath = "0.10.2"
|
oath = "0.10.2"
|
||||||
@@ -78,20 +78,20 @@ jsonwebtoken = "6.0.1"
|
|||||||
u2f = "0.1.6"
|
u2f = "0.1.6"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { version = "0.6.1", features = ["online", "online-tokio"], default-features = false }
|
yubico = { version = "0.7.1", features = ["online-tokio"], default-features = false }
|
||||||
|
|
||||||
# A `dotenv` implementation for Rust
|
# A `dotenv` implementation for Rust
|
||||||
dotenv = { version = "0.14.1", default-features = false }
|
dotenv = { version = "0.15.0", default-features = false }
|
||||||
|
|
||||||
# Lazy static macro
|
# Lazy static macro
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
|
||||||
# More derives
|
# More derives
|
||||||
derive_more = "0.15.0"
|
derive_more = "0.99.2"
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.8"
|
num-traits = "0.2.10"
|
||||||
num-derive = "0.2.5"
|
num-derive = "0.3.0"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = "0.9.2"
|
lettre = "0.9.2"
|
||||||
@@ -100,22 +100,27 @@ native-tls = "0.2.3"
|
|||||||
quoted_printable = "0.4.1"
|
quoted_printable = "0.4.1"
|
||||||
|
|
||||||
# Template library
|
# Template library
|
||||||
handlebars = "2.0.2"
|
handlebars = "=2.0.2"
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# For favicon extraction from main website
|
||||||
soup = "0.4.1"
|
soup = "0.4.1"
|
||||||
regex = "1.3.1"
|
regex = "1.3.1"
|
||||||
|
data-url = "0.1.0"
|
||||||
|
|
||||||
# Required for SSL support for PostgreSQL
|
# Required for SSL support for PostgreSQL
|
||||||
openssl = { version = "0.10.24", optional = true }
|
openssl = { version = "0.10.26", optional = true }
|
||||||
|
|
||||||
# URL encoding library
|
# URL encoding library
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Add support for Timestamp type
|
|
||||||
rmp = { git = 'https://github.com/3Hren/msgpack-rust', rev = 'd6c6c672e470341207ed9feb69b56322b5597a11' }
|
|
||||||
|
|
||||||
# Use newest ring
|
# Use newest ring
|
||||||
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dbcb0a75b9556763ac3ab708f40c8f8ed75f1a1e' }
|
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'b95b6765e1cc8be7c1e7eaef8a9d9ad940b0ac13' }
|
||||||
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dbcb0a75b9556763ac3ab708f40c8f8ed75f1a1e' }
|
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'b95b6765e1cc8be7c1e7eaef8a9d9ad940b0ac13' }
|
||||||
|
|
||||||
|
# Use git version for timeout fix #706
|
||||||
|
lettre = { git = 'https://github.com/lettre/lettre', rev = '24d694db3be017d82b1cdc8bf9da601420b31bb0' }
|
||||||
|
lettre_email = { git = 'https://github.com/lettre/lettre', rev = '24d694db3be017d82b1cdc8bf9da601420b31bb0' }
|
||||||
|
|
||||||
|
# For favicon extraction from main website
|
||||||
|
data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = '7f1bd6ce1c2fde599a757302a843a60e714c5f72' }
|
||||||
|
@@ -50,6 +50,11 @@ See the [bitwarden_rs wiki](https://github.com/dani-garcia/bitwarden_rs/wiki) fo
|
|||||||
|
|
||||||
## Get in touch
|
## Get in touch
|
||||||
|
|
||||||
To ask an question, [raising an issue](https://github.com/dani-garcia/bitwarden_rs/issues/new) is fine, also please report any bugs spotted here.
|
To ask a question, [raising an issue](https://github.com/dani-garcia/bitwarden_rs/issues/new) is fine. Please also report any bugs spotted here.
|
||||||
|
|
||||||
If you prefer to chat, we're usually hanging around at [#bitwarden_rs:matrix.org](https://matrix.to/#/#bitwarden_rs:matrix.org) room on Matrix. Feel free to join us!
|
If you prefer to chat, we're usually hanging around at [#bitwarden_rs:matrix.org](https://matrix.to/#/#bitwarden_rs:matrix.org) room on Matrix. Feel free to join us!
|
||||||
|
|
||||||
|
### Sponsors
|
||||||
|
Thanks for your contribution to the project!
|
||||||
|
|
||||||
|
- [@Skaronator](https://github.com/Skaronator)
|
||||||
|
@@ -4,7 +4,7 @@ pool:
|
|||||||
steps:
|
steps:
|
||||||
- script: |
|
- script: |
|
||||||
ls -la
|
ls -la
|
||||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $(cat rust-toolchain)
|
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $(cat rust-toolchain) --profile=minimal
|
||||||
echo "##vso[task.prependpath]$HOME/.cargo/bin"
|
echo "##vso[task.prependpath]$HOME/.cargo/bin"
|
||||||
displayName: 'Install Rust'
|
displayName: 'Install Rust'
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ steps:
|
|||||||
cargo -V
|
cargo -V
|
||||||
displayName: Query rust and cargo versions
|
displayName: Query rust and cargo versions
|
||||||
|
|
||||||
- script : cargo build --features "sqlite"
|
- script : cargo test --features "sqlite"
|
||||||
displayName: 'Build project with sqlite backend'
|
displayName: 'Test project with sqlite backend'
|
||||||
|
|
||||||
- script : cargo build --features "mysql"
|
- script : cargo test --features "mysql"
|
||||||
displayName: 'Build project with mysql backend'
|
displayName: 'Test project with mysql backend'
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -23,11 +23,14 @@ RUN ls
|
|||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
# we need the Rust compiler and Cargo tooling
|
# we need the Rust compiler and Cargo tooling
|
||||||
FROM rust:1.36 as build
|
FROM rust:1.40 as build
|
||||||
|
|
||||||
# set mysql backend
|
# set mysql backend
|
||||||
ARG DB=mysql
|
ARG DB=mysql
|
||||||
|
|
||||||
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
@@ -63,12 +66,12 @@ COPY . .
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN rustup target add aarch64-unknown-linux-gnu
|
RUN rustup target add aarch64-unknown-linux-gnu
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu -v
|
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/aarch64-debian:stretch
|
FROM balenalib/aarch64-debian:buster
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -103,4 +106,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -23,11 +23,14 @@ RUN ls
|
|||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
# we need the Rust compiler and Cargo tooling
|
# we need the Rust compiler and Cargo tooling
|
||||||
FROM rust:1.36 as build
|
FROM rust:1.40 as build
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward comaptibility
|
# set sqlite as default for DB ARG for backward comaptibility
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite
|
||||||
|
|
||||||
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
@@ -49,8 +52,7 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
|||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:arm64 \
|
libssl-dev:arm64 \
|
||||||
libc6-dev:arm64 \
|
libc6-dev:arm64
|
||||||
libmariadb-dev:arm64
|
|
||||||
|
|
||||||
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
|
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
|
||||||
ENV CROSS_COMPILE="1"
|
ENV CROSS_COMPILE="1"
|
||||||
@@ -63,12 +65,12 @@ COPY . .
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN rustup target add aarch64-unknown-linux-gnu
|
RUN rustup target add aarch64-unknown-linux-gnu
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu -v
|
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/aarch64-debian:stretch
|
FROM balenalib/aarch64-debian:buster
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -103,4 +105,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -23,16 +23,13 @@ RUN ls
|
|||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
# we need the Rust compiler and Cargo tooling
|
# we need the Rust compiler and Cargo tooling
|
||||||
FROM rust:1.36 as build
|
FROM rust:1.40 as build
|
||||||
|
|
||||||
# set mysql backend
|
# set mysql backend
|
||||||
ARG DB=mysql
|
ARG DB=mysql
|
||||||
|
|
||||||
# Using bundled SQLite, no need to install it
|
# Don't download rust docs
|
||||||
# RUN apt-get update && apt-get install -y\
|
RUN rustup set profile minimal
|
||||||
# --no-install-recommends \
|
|
||||||
# sqlite3\
|
|
||||||
# && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install MySQL package
|
# Install MySQL package
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
@@ -69,7 +66,7 @@ RUN cargo build --features ${DB} --release
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM debian:stretch-slim
|
FROM debian:buster-slim
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -100,4 +97,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -22,11 +22,14 @@ RUN ls
|
|||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# Musl build image for statically compiled binary
|
# Musl build image for statically compiled binary
|
||||||
FROM clux/muslrust:nightly-2019-07-08 as build
|
FROM clux/muslrust:nightly-2019-12-19 as build
|
||||||
|
|
||||||
# set mysql backend
|
# set mysql backend
|
||||||
ARG DB=mysql
|
ARG DB=mysql
|
||||||
|
|
||||||
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
|
|
||||||
# Install needed libraries
|
# Install needed libraries
|
||||||
@@ -52,7 +55,7 @@ RUN cargo build --features ${DB} --release
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.10
|
FROM alpine:3.11
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -82,4 +85,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -23,11 +23,14 @@ RUN ls
|
|||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
# we need the Rust compiler and Cargo tooling
|
# we need the Rust compiler and Cargo tooling
|
||||||
FROM rust:1.36 as build
|
FROM rust:1.40 as build
|
||||||
|
|
||||||
# set mysql backend
|
# set mysql backend
|
||||||
ARG DB=postgresql
|
ARG DB=postgresql
|
||||||
|
|
||||||
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
# Using bundled SQLite, no need to install it
|
# Using bundled SQLite, no need to install it
|
||||||
# RUN apt-get update && apt-get install -y\
|
# RUN apt-get update && apt-get install -y\
|
||||||
# --no-install-recommends \
|
# --no-install-recommends \
|
||||||
@@ -69,7 +72,7 @@ RUN cargo build --features ${DB} --release
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM debian:stretch-slim
|
FROM debian:buster-slim
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -101,4 +104,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -22,11 +22,14 @@ RUN ls
|
|||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# Musl build image for statically compiled binary
|
# Musl build image for statically compiled binary
|
||||||
FROM clux/muslrust:nightly-2019-07-08 as build
|
FROM clux/muslrust:nightly-2019-12-19 as build
|
||||||
|
|
||||||
# set mysql backend
|
# set postgresql backend
|
||||||
ARG DB=postgresql
|
ARG DB=postgresql
|
||||||
|
|
||||||
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
|
|
||||||
# Install needed libraries
|
# Install needed libraries
|
||||||
@@ -52,7 +55,7 @@ RUN cargo build --features ${DB} --release
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.10
|
FROM alpine:3.11
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -83,4 +86,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -23,22 +23,13 @@ RUN ls
|
|||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
# we need the Rust compiler and Cargo tooling
|
# we need the Rust compiler and Cargo tooling
|
||||||
FROM rust:1.36 as build
|
FROM rust:1.40 as build
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward comaptibility
|
# set sqlite as default for DB ARG for backward comaptibility
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite
|
||||||
|
|
||||||
# Using bundled SQLite, no need to install it
|
# Don't download rust docs
|
||||||
# RUN apt-get update && apt-get install -y\
|
RUN rustup set profile minimal
|
||||||
# --no-install-recommends \
|
|
||||||
# sqlite3 \
|
|
||||||
# && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install MySQL package
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libmariadb-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin app
|
RUN USER=root cargo new --bin app
|
||||||
@@ -69,7 +60,7 @@ RUN cargo build --features ${DB} --release
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM debian:stretch-slim
|
FROM debian:buster-slim
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -100,4 +91,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -22,18 +22,15 @@ RUN ls
|
|||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# Musl build image for statically compiled binary
|
# Musl build image for statically compiled binary
|
||||||
FROM clux/muslrust:nightly-2019-07-08 as build
|
FROM clux/muslrust:nightly-2019-12-19 as build
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward comaptibility
|
# set sqlite as default for DB ARG for backward comaptibility
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite
|
||||||
|
|
||||||
ENV USER "root"
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
# Install needed libraries
|
ENV USER "root"
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libmysqlclient-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -52,7 +49,7 @@ RUN cargo build --features ${DB} --release
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.10
|
FROM alpine:3.11
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -83,4 +80,5 @@ HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
|||||||
|
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -23,11 +23,14 @@ RUN ls
|
|||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
# we need the Rust compiler and Cargo tooling
|
# we need the Rust compiler and Cargo tooling
|
||||||
FROM rust:1.36 as build
|
FROM rust:1.40 as build
|
||||||
|
|
||||||
# set mysql backend
|
# set mysql backend
|
||||||
ARG DB=mysql
|
ARG DB=mysql
|
||||||
|
|
||||||
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
@@ -63,12 +66,12 @@ COPY . .
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN rustup target add arm-unknown-linux-gnueabi
|
RUN rustup target add arm-unknown-linux-gnueabi
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi -v
|
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/rpi-debian:stretch
|
FROM balenalib/rpi-debian:buster
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -103,4 +106,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -23,11 +23,14 @@ RUN ls
|
|||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
# we need the Rust compiler and Cargo tooling
|
# we need the Rust compiler and Cargo tooling
|
||||||
FROM rust:1.36 as build
|
FROM rust:1.40 as build
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward comaptibility
|
# set sqlite as default for DB ARG for backward comaptibility
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite
|
||||||
|
|
||||||
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
@@ -49,8 +52,7 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
|||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:armel \
|
libssl-dev:armel \
|
||||||
libc6-dev:armel \
|
libc6-dev:armel
|
||||||
libmariadb-dev:armel
|
|
||||||
|
|
||||||
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
|
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
|
||||||
ENV CROSS_COMPILE="1"
|
ENV CROSS_COMPILE="1"
|
||||||
@@ -63,12 +65,12 @@ COPY . .
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN rustup target add arm-unknown-linux-gnueabi
|
RUN rustup target add arm-unknown-linux-gnueabi
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi -v
|
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/rpi-debian:stretch
|
FROM balenalib/rpi-debian:buster
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -103,4 +105,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -23,11 +23,14 @@ RUN ls
|
|||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
# we need the Rust compiler and Cargo tooling
|
# we need the Rust compiler and Cargo tooling
|
||||||
FROM rust:1.36 as build
|
FROM rust:1.40 as build
|
||||||
|
|
||||||
# set mysql backend
|
# set mysql backend
|
||||||
ARG DB=mysql
|
ARG DB=mysql
|
||||||
|
|
||||||
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
@@ -64,12 +67,12 @@ COPY . .
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
RUN rustup target add armv7-unknown-linux-gnueabihf
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf -v
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-debian:stretch
|
FROM balenalib/armv7hf-debian:buster
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -104,4 +107,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
# 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 #######################
|
||||||
FROM alpine:3.10 as vault
|
FROM alpine:3.11 as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.12.0"
|
ENV VAULT_VERSION "v2.12.0b"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
@@ -23,11 +23,14 @@ RUN ls
|
|||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
# We need to use the Rust build image, because
|
||||||
# we need the Rust compiler and Cargo tooling
|
# we need the Rust compiler and Cargo tooling
|
||||||
FROM rust:1.36 as build
|
FROM rust:1.40 as build
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward comaptibility
|
# set sqlite as default for DB ARG for backward comaptibility
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite
|
||||||
|
|
||||||
|
# Don't download rust docs
|
||||||
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
@@ -49,8 +52,7 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
|||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:armhf \
|
libssl-dev:armhf \
|
||||||
libc6-dev:armhf \
|
libc6-dev:armhf
|
||||||
libmariadb-dev:armhf
|
|
||||||
|
|
||||||
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
|
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
|
||||||
ENV CROSS_COMPILE="1"
|
ENV CROSS_COMPILE="1"
|
||||||
@@ -63,12 +65,12 @@ COPY . .
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
RUN rustup target add armv7-unknown-linux-gnueabihf
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf -v
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-debian:stretch
|
FROM balenalib/armv7hf-debian:buster
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -103,4 +105,5 @@ COPY docker/healthcheck.sh ./healthcheck.sh
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
CMD ["./bitwarden_rs"]
|
WORKDIR /
|
||||||
|
CMD ["/bitwarden_rs"]
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL;
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN verified_at TIMESTAMP DEFAULT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN last_verifying_at TIMESTAMP DEFAULT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL;
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE users ADD COLUMN email_new TEXT DEFAULT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN email_new_token TEXT DEFAULT NULL;
|
@@ -1 +1 @@
|
|||||||
nightly-2019-08-27
|
nightly-2019-12-19
|
@@ -1 +1,2 @@
|
|||||||
|
version = "Two"
|
||||||
max_width = 120
|
max_width = 120
|
||||||
|
@@ -26,6 +26,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
post_admin_login,
|
post_admin_login,
|
||||||
admin_page,
|
admin_page,
|
||||||
invite_user,
|
invite_user,
|
||||||
|
logout,
|
||||||
delete_user,
|
delete_user,
|
||||||
deauth_user,
|
deauth_user,
|
||||||
remove_2fa,
|
remove_2fa,
|
||||||
@@ -109,6 +110,7 @@ struct AdminTemplateData {
|
|||||||
users: Vec<Value>,
|
users: Vec<Value>,
|
||||||
config: Value,
|
config: Value,
|
||||||
can_backup: bool,
|
can_backup: bool,
|
||||||
|
logged_in: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdminTemplateData {
|
impl AdminTemplateData {
|
||||||
@@ -119,6 +121,7 @@ impl AdminTemplateData {
|
|||||||
users,
|
users,
|
||||||
config: CONFIG.prepare_json(),
|
config: CONFIG.prepare_json(),
|
||||||
can_backup: *CAN_BACKUP,
|
can_backup: *CAN_BACKUP,
|
||||||
|
logged_in: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +169,12 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/logout")]
|
||||||
|
fn logout(mut cookies: Cookies) -> Result<Redirect, ()> {
|
||||||
|
cookies.remove(Cookie::named(COOKIE_NAME));
|
||||||
|
Ok(Redirect::to(ADMIN_PATH))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/users")]
|
#[get("/users")]
|
||||||
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
|
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
|
||||||
let users = User::get_all(&conn);
|
let users = User::get_all(&conn);
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
|
use chrono::Utc;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
|
||||||
use crate::db::models::*;
|
use crate::db::models::*;
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
|
||||||
use crate::auth::{decode_invite, Headers};
|
use crate::auth::{decode_delete, decode_invite, decode_verify_email, Headers};
|
||||||
|
use crate::crypto;
|
||||||
use crate::mail;
|
use crate::mail;
|
||||||
|
|
||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
@@ -25,6 +27,10 @@ pub fn routes() -> Vec<Route> {
|
|||||||
post_sstamp,
|
post_sstamp,
|
||||||
post_email_token,
|
post_email_token,
|
||||||
post_email,
|
post_email,
|
||||||
|
post_verify_email,
|
||||||
|
post_verify_email_token,
|
||||||
|
post_delete_recover,
|
||||||
|
post_delete_recover_token,
|
||||||
delete_account,
|
delete_account,
|
||||||
post_delete_account,
|
post_delete_account,
|
||||||
revision_date,
|
revision_date,
|
||||||
@@ -62,7 +68,11 @@ 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() {
|
||||||
err!("User already exists")
|
if CONFIG.signups_allowed() {
|
||||||
|
err!("User already exists")
|
||||||
|
} else {
|
||||||
|
err!("Registration not allowed or user already exists")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(token) = data.Token {
|
if let Some(token) = data.Token {
|
||||||
@@ -82,14 +92,14 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
} else if CONFIG.signups_allowed() {
|
} else if CONFIG.signups_allowed() {
|
||||||
err!("Account with this email already exists")
|
err!("Account with this email already exists")
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed")
|
err!("Registration not allowed or user already exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if CONFIG.signups_allowed() || Invitation::take(&data.Email, &conn) {
|
if CONFIG.signups_allowed() || Invitation::take(&data.Email, &conn) || CONFIG.can_signup_user(&data.Email) {
|
||||||
User::new(data.Email.clone())
|
User::new(data.Email.clone())
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed")
|
err!("Registration not allowed or user already exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -122,6 +132,20 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
user.public_key = Some(keys.PublicKey);
|
user.public_key = Some(keys.PublicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
if CONFIG.signups_verify() {
|
||||||
|
if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid) {
|
||||||
|
error!("Error sending welcome email: {:#?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.last_verifying_at = Some(user.created_at);
|
||||||
|
} else {
|
||||||
|
if let Err(e) = mail::send_welcome(&user.email) {
|
||||||
|
error!("Error sending welcome email: {:#?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,8 +361,9 @@ struct EmailTokenData {
|
|||||||
#[post("/accounts/email-token", data = "<data>")]
|
#[post("/accounts/email-token", data = "<data>")]
|
||||||
fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||||
let data: EmailTokenData = data.into_inner().data;
|
let data: EmailTokenData = data.into_inner().data;
|
||||||
|
let mut user = headers.user;
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +371,21 @@ fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: Db
|
|||||||
err!("Email already in use");
|
err!("Email already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
if !CONFIG.signups_allowed() && !CONFIG.can_signup_user(&data.NewEmail) {
|
||||||
|
err!("Email cannot be changed to this address");
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = crypto::generate_token(6)?;
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
if let Err(e) = mail::send_change_email(&data.NewEmail, &token) {
|
||||||
|
error!("Error sending change-email email: {:#?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.email_new = Some(data.NewEmail);
|
||||||
|
user.email_new_token = Some(token);
|
||||||
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -357,8 +396,7 @@ struct ChangeEmailData {
|
|||||||
|
|
||||||
Key: String,
|
Key: String,
|
||||||
NewMasterPasswordHash: String,
|
NewMasterPasswordHash: String,
|
||||||
#[serde(rename = "Token")]
|
Token: NumberOrString,
|
||||||
_Token: NumberOrString,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/email", data = "<data>")]
|
#[post("/accounts/email", data = "<data>")]
|
||||||
@@ -374,7 +412,33 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
|||||||
err!("Email already in use");
|
err!("Email already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match user.email_new {
|
||||||
|
Some(ref val) => {
|
||||||
|
if val != &data.NewEmail {
|
||||||
|
err!("Email change mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => err!("No email change pending"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
// Only check the token if we sent out an email...
|
||||||
|
match user.email_new_token {
|
||||||
|
Some(ref val) => {
|
||||||
|
if *val != data.Token.into_string() {
|
||||||
|
err!("Token mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => err!("No email change pending"),
|
||||||
|
}
|
||||||
|
user.verified_at = Some(Utc::now().naive_utc());
|
||||||
|
} else {
|
||||||
|
user.verified_at = None;
|
||||||
|
}
|
||||||
|
|
||||||
user.email = data.NewEmail;
|
user.email = data.NewEmail;
|
||||||
|
user.email_new = None;
|
||||||
|
user.email_new_token = None;
|
||||||
|
|
||||||
user.set_password(&data.NewMasterPasswordHash);
|
user.set_password(&data.NewMasterPasswordHash);
|
||||||
user.akey = data.Key;
|
user.akey = data.Key;
|
||||||
@@ -382,6 +446,108 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
|||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/verify-email")]
|
||||||
|
fn post_verify_email(headers: Headers, _conn: DbConn) -> EmptyResult {
|
||||||
|
let user = headers.user;
|
||||||
|
|
||||||
|
if !CONFIG.mail_enabled() {
|
||||||
|
err!("Cannot verify email address");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) {
|
||||||
|
error!("Error sending delete account email: {:#?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct VerifyEmailTokenData {
|
||||||
|
UserId: String,
|
||||||
|
Token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/verify-email-token", data = "<data>")]
|
||||||
|
fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>, conn: DbConn) -> EmptyResult {
|
||||||
|
let data: VerifyEmailTokenData = data.into_inner().data;
|
||||||
|
|
||||||
|
let mut user = match User::find_by_uuid(&data.UserId, &conn) {
|
||||||
|
Some(user) => user,
|
||||||
|
None => err!("User doesn't exist"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let claims = match decode_verify_email(&data.Token) {
|
||||||
|
Ok(claims) => claims,
|
||||||
|
Err(_) => err!("Invalid claim"),
|
||||||
|
};
|
||||||
|
if claims.sub != user.uuid {
|
||||||
|
err!("Invalid claim");
|
||||||
|
}
|
||||||
|
user.verified_at = Some(Utc::now().naive_utc());
|
||||||
|
user.last_verifying_at = None;
|
||||||
|
user.login_verify_count = 0;
|
||||||
|
if let Err(e) = user.save(&conn) {
|
||||||
|
error!("Error saving email verification: {:#?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct DeleteRecoverData {
|
||||||
|
Email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/delete-recover", data = "<data>")]
|
||||||
|
fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>, conn: DbConn) -> EmptyResult {
|
||||||
|
let data: DeleteRecoverData = data.into_inner().data;
|
||||||
|
|
||||||
|
let user = User::find_by_mail(&data.Email, &conn);
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
if let Some(user) = user {
|
||||||
|
if let Err(e) = mail::send_delete_account(&user.email, &user.uuid) {
|
||||||
|
error!("Error sending delete account email: {:#?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
// We don't support sending emails, but we shouldn't allow anybody
|
||||||
|
// to delete accounts without at least logging in... And if the user
|
||||||
|
// cannot remember their password then they will need to contact
|
||||||
|
// the administrator to delete it...
|
||||||
|
err!("Please contact the administrator to delete your account");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct DeleteRecoverTokenData {
|
||||||
|
UserId: String,
|
||||||
|
Token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/delete-recover-token", data = "<data>")]
|
||||||
|
fn post_delete_recover_token(data: JsonUpcase<DeleteRecoverTokenData>, conn: DbConn) -> EmptyResult {
|
||||||
|
let data: DeleteRecoverTokenData = data.into_inner().data;
|
||||||
|
|
||||||
|
let user = match User::find_by_uuid(&data.UserId, &conn) {
|
||||||
|
Some(user) => user,
|
||||||
|
None => err!("User doesn't exist"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let claims = match decode_delete(&data.Token) {
|
||||||
|
Ok(claims) => claims,
|
||||||
|
Err(_) => err!("Invalid claim"),
|
||||||
|
};
|
||||||
|
if claims.sub != user.uuid {
|
||||||
|
err!("Invalid claim");
|
||||||
|
}
|
||||||
|
user.delete(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/accounts/delete", data = "<data>")]
|
#[post("/accounts/delete", data = "<data>")]
|
||||||
fn post_delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn post_delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||||
delete_account(data, headers, conn)
|
delete_account(data, headers, conn)
|
||||||
|
@@ -88,7 +88,7 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
let domains_json = if data.exclude_domains {
|
let domains_json = if data.exclude_domains {
|
||||||
Value::Null
|
Value::Null
|
||||||
} else {
|
} else {
|
||||||
api::core::get_eq_domains(headers).unwrap().into_inner()
|
api::core::_get_eq_domains(headers, true).unwrap().into_inner()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
|
@@ -59,7 +59,7 @@ pub struct FolderData {
|
|||||||
fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
let data: FolderData = data.into_inner().data;
|
let data: FolderData = data.into_inner().data;
|
||||||
|
|
||||||
let mut folder = Folder::new(headers.user.uuid.clone(), data.Name);
|
let mut folder = Folder::new(headers.user.uuid, data.Name);
|
||||||
|
|
||||||
folder.save(&conn)?;
|
folder.save(&conn)?;
|
||||||
nt.send_folder_update(UpdateType::FolderCreate, &folder);
|
nt.send_folder_update(UpdateType::FolderCreate, &folder);
|
||||||
|
@@ -81,6 +81,10 @@ const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
|
|||||||
|
|
||||||
#[get("/settings/domains")]
|
#[get("/settings/domains")]
|
||||||
fn get_eq_domains(headers: Headers) -> JsonResult {
|
fn get_eq_domains(headers: Headers) -> JsonResult {
|
||||||
|
_get_eq_domains(headers, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _get_eq_domains(headers: Headers, no_excluded: bool) -> JsonResult {
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
|
|
||||||
@@ -93,6 +97,10 @@ fn get_eq_domains(headers: Headers) -> JsonResult {
|
|||||||
global.Excluded = excluded_globals.contains(&global.Type);
|
global.Excluded = excluded_globals.contains(&global.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if no_excluded {
|
||||||
|
globals.retain(|g| !g.Excluded);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"EquivalentDomains": equivalent_domains,
|
"EquivalentDomains": equivalent_domains,
|
||||||
"GlobalEquivalentDomains": globals,
|
"GlobalEquivalentDomains": globals,
|
||||||
@@ -141,7 +149,9 @@ fn hibp_breach(username: String) -> JsonResult {
|
|||||||
use reqwest::{header::USER_AGENT, Client};
|
use reqwest::{header::USER_AGENT, Client};
|
||||||
|
|
||||||
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
||||||
let res = Client::new()
|
let hibp_client = Client::builder().use_sys_proxy().build()?;
|
||||||
|
|
||||||
|
let res = hibp_client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.header(USER_AGENT, user_agent)
|
.header(USER_AGENT, user_agent)
|
||||||
.header("hibp-api-key", api_key)
|
.header("hibp-api-key", api_key)
|
||||||
@@ -156,9 +166,17 @@ fn hibp_breach(username: String) -> JsonResult {
|
|||||||
Ok(Json(value))
|
Ok(Json(value))
|
||||||
} else {
|
} else {
|
||||||
Ok(Json(json!([{
|
Ok(Json(json!([{
|
||||||
"title": "--- Error! ---",
|
"Name": "HaveIBeenPwned",
|
||||||
"description": "HaveIBeenPwned API key not set! Go to https://haveibeenpwned.com/API/Key",
|
"Title": "Manual HIBP Check",
|
||||||
"logopath": "/bwrs_static/error-x.svg"
|
"Domain": "haveibeenpwned.com",
|
||||||
|
"BreachDate": "2019-08-18T00:00:00Z",
|
||||||
|
"AddedDate": "2019-08-18T00:00:00Z",
|
||||||
|
"Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username),
|
||||||
|
"LogoPath": "/bwrs_static/hibp.png",
|
||||||
|
"PwnCount": 0,
|
||||||
|
"DataClasses": [
|
||||||
|
"Error - No API key set!"
|
||||||
|
]
|
||||||
}])))
|
}])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -77,7 +77,7 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
|
|||||||
let data: OrgData = data.into_inner().data;
|
let data: OrgData = data.into_inner().data;
|
||||||
|
|
||||||
let org = Organization::new(data.Name, data.BillingEmail);
|
let org = Organization::new(data.Name, data.BillingEmail);
|
||||||
let mut user_org = UserOrganization::new(headers.user.uuid.clone(), org.uuid.clone());
|
let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone());
|
||||||
let collection = Collection::new(org.uuid.clone(), data.CollectionName);
|
let collection = Collection::new(org.uuid.clone(), data.CollectionName);
|
||||||
|
|
||||||
user_org.akey = data.Key;
|
user_org.akey = data.Key;
|
||||||
@@ -221,7 +221,7 @@ fn post_organization_collections(
|
|||||||
None => err!("Can't find organization details"),
|
None => err!("Can't find organization details"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let collection = Collection::new(org.uuid.clone(), data.Name);
|
let collection = Collection::new(org.uuid, data.Name);
|
||||||
collection.save(&conn)?;
|
collection.save(&conn)?;
|
||||||
|
|
||||||
Ok(Json(collection.to_json()))
|
Ok(Json(collection.to_json()))
|
||||||
@@ -262,7 +262,7 @@ fn post_organization_collection_update(
|
|||||||
err!("Collection is not owned by organization");
|
err!("Collection is not owned by organization");
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.name = data.Name.clone();
|
collection.name = data.Name;
|
||||||
collection.save(&conn)?;
|
collection.save(&conn)?;
|
||||||
|
|
||||||
Ok(Json(collection.to_json()))
|
Ok(Json(collection.to_json()))
|
||||||
@@ -581,7 +581,7 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
|
|||||||
Some(headers.user.email),
|
Some(headers.user.email),
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
let invitation = Invitation::new(user.email.clone());
|
let invitation = Invitation::new(user.email);
|
||||||
invitation.save(&conn)?;
|
invitation.save(&conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,6 +11,8 @@ use crate::db::{
|
|||||||
DbConn,
|
DbConn,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use crate::config::CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![
|
||||||
generate_authenticator,
|
generate_authenticator,
|
||||||
@@ -73,14 +75,10 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
|||||||
err!("Invalid key length")
|
err!("Invalid key length")
|
||||||
}
|
}
|
||||||
|
|
||||||
let type_ = TwoFactorType::Authenticator;
|
// Validate the token provided with the key, and save new twofactor
|
||||||
let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
|
validate_totp_code(&user.uuid, token, &key.to_uppercase(), &conn)?;
|
||||||
|
|
||||||
// Validate the token provided with the key
|
|
||||||
validate_totp_code(token, &twofactor.data)?;
|
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &conn);
|
||||||
twofactor.save(&conn)?;
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
@@ -94,27 +92,62 @@ fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers
|
|||||||
activate_authenticator(data, headers, conn)
|
activate_authenticator(data, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
|
pub fn validate_totp_code_str(user_uuid: &str, totp_code: &str, secret: &str, 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(totp_code, secret)
|
validate_totp_code(user_uuid, totp_code, secret, &conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
|
pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &DbConn) -> EmptyResult {
|
||||||
use oath::{totp_raw_now, 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()) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => err!("Invalid TOTP secret"),
|
Err(_) => err!("Invalid TOTP secret"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
|
let mut twofactor = match TwoFactor::find_by_user_and_type(&user_uuid, TwoFactorType::Authenticator as i32, &conn) {
|
||||||
if generated != totp_code {
|
Some(tf) => tf,
|
||||||
err!("Invalid TOTP code");
|
_ => TwoFactor::new(user_uuid.to_string(), TwoFactorType::Authenticator, secret.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the current system time in UNIX Epoch (UTC)
|
||||||
|
let current_time = chrono::Utc::now();
|
||||||
|
let current_timestamp = current_time.timestamp();
|
||||||
|
|
||||||
|
// The amount of steps back and forward in time
|
||||||
|
// Also check if we need to disable time drifted TOTP codes.
|
||||||
|
// If that is the case, we set the steps to 0 so only the current TOTP is valid.
|
||||||
|
let steps: i64 = if CONFIG.authenticator_disable_time_drift() { 0 } else { 1 };
|
||||||
|
|
||||||
|
for step in -steps..=steps {
|
||||||
|
let time_step = current_timestamp / 30i64 + step;
|
||||||
|
// We need to calculate the time offsite and cast it as an i128.
|
||||||
|
// Else we can't do math with it on a default u64 variable.
|
||||||
|
let time = (current_timestamp + step * 30i64) as u64;
|
||||||
|
let generated = totp_raw_custom_time(&decoded_secret, 6, 0, 30, time, &HashType::SHA1);
|
||||||
|
|
||||||
|
// Check the the given code equals the generated and if the time_step is larger then the one last used.
|
||||||
|
if generated == totp_code && time_step > twofactor.last_used as i64 {
|
||||||
|
// If the step does not equals 0 the time is drifted either server or client side.
|
||||||
|
if step != 0 {
|
||||||
|
info!("TOTP Time drift detected. The step offset is {}", step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the last used time step so only totp time steps higher then this one are allowed.
|
||||||
|
// This will also save a newly created twofactor if the code is correct.
|
||||||
|
twofactor.last_used = time_step as i32;
|
||||||
|
twofactor.save(&conn)?;
|
||||||
|
return Ok(());
|
||||||
|
} 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);
|
||||||
|
err!(format!("Invalid TOTP code! Server time: {}", current_time.format("%F %T UTC")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
// Else no valide code received, deny access
|
||||||
|
err!(format!("Invalid TOTP code! Server time: {}", current_time.format("%F %T UTC")));
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ use rocket::Route;
|
|||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
|
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};
|
||||||
use crate::auth::Headers;
|
use crate::auth::Headers;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
@@ -15,11 +16,7 @@ use crate::error::MapResult;
|
|||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![get_duo, activate_duo, activate_duo_put,]
|
||||||
get_duo,
|
|
||||||
activate_duo,
|
|
||||||
activate_duo_put,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -152,8 +149,9 @@ fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
|
|||||||
#[post("/two-factor/duo", data = "<data>")]
|
#[post("/two-factor/duo", data = "<data>")]
|
||||||
fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let data: EnableDuoData = data.into_inner().data;
|
let data: EnableDuoData = data.into_inner().data;
|
||||||
|
let mut user = headers.user;
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,9 +165,11 @@ fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn)
|
|||||||
};
|
};
|
||||||
|
|
||||||
let type_ = TwoFactorType::Duo;
|
let type_ = TwoFactorType::Duo;
|
||||||
let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
|
let twofactor = TwoFactor::new(user.uuid.clone(), type_, data_str);
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
|
_generate_recover_code(&mut user, &conn);
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"Host": data.host,
|
"Host": data.host,
|
||||||
|
@@ -2,6 +2,7 @@ use rocket::Route;
|
|||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
|
use crate::api::core::two_factor::_generate_recover_code;
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
||||||
use crate::auth::Headers;
|
use crate::auth::Headers;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
@@ -17,12 +18,7 @@ use chrono::{Duration, NaiveDateTime, Utc};
|
|||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![get_email, send_email_login, send_email, email,]
|
||||||
get_email,
|
|
||||||
send_email_login,
|
|
||||||
send_email,
|
|
||||||
email,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -55,10 +51,18 @@ fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> Empty
|
|||||||
err!("Email 2FA is disabled")
|
err!("Email 2FA is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
let type_ = TwoFactorType::Email as i32;
|
send_token(&user.uuid, &conn)?;
|
||||||
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the token, save the data for later verification and send email to user
|
||||||
|
pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
let type_ = TwoFactorType::Email as i32;
|
||||||
|
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, &conn)?;
|
||||||
|
|
||||||
|
let generated_token = crypto::generate_token(CONFIG.email_token_size())?;
|
||||||
|
|
||||||
let generated_token = generate_token(CONFIG.email_token_size())?;
|
|
||||||
let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
|
let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
|
||||||
twofactor_data.set_token(generated_token);
|
twofactor_data.set_token(generated_token);
|
||||||
twofactor.data = twofactor_data.to_json();
|
twofactor.data = twofactor_data.to_json();
|
||||||
@@ -100,22 +104,6 @@ struct SendEmailData {
|
|||||||
MasterPasswordHash: String,
|
MasterPasswordHash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn generate_token(token_size: u32) -> Result<String, Error> {
|
|
||||||
if token_size > 19 {
|
|
||||||
err!("Generating token failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8 bytes to create an u64 for up to 19 token digits
|
|
||||||
let bytes = crypto::get_random(vec![0; 8]);
|
|
||||||
let mut bytes_array = [0u8; 8];
|
|
||||||
bytes_array.copy_from_slice(&bytes);
|
|
||||||
|
|
||||||
let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size);
|
|
||||||
let token = format!("{:0size$}", number, size = token_size as usize);
|
|
||||||
Ok(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a verification email to the specified email address to check whether it exists/belongs to user.
|
/// Send a verification email to the specified email address to check whether it exists/belongs to user.
|
||||||
#[post("/two-factor/send-email", data = "<data>")]
|
#[post("/two-factor/send-email", data = "<data>")]
|
||||||
fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||||
@@ -136,7 +124,7 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -
|
|||||||
tf.delete(&conn)?;
|
tf.delete(&conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let generated_token = generate_token(CONFIG.email_token_size())?;
|
let generated_token = crypto::generate_token(CONFIG.email_token_size())?;
|
||||||
let twofactor_data = EmailTokenData::new(data.Email, generated_token);
|
let twofactor_data = EmailTokenData::new(data.Email, generated_token);
|
||||||
|
|
||||||
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
||||||
@@ -164,7 +152,7 @@ struct EmailData {
|
|||||||
#[put("/two-factor/email", data = "<data>")]
|
#[put("/two-factor/email", data = "<data>")]
|
||||||
fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let data: EmailData = data.into_inner().data;
|
let data: EmailData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
@@ -189,6 +177,8 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
|
|||||||
twofactor.data = email_data.to_json();
|
twofactor.data = email_data.to_json();
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
|
_generate_recover_code(&mut user, &conn);
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Email": email_data.email,
|
"Email": email_data.email,
|
||||||
"Enabled": "true",
|
"Enabled": "true",
|
||||||
@@ -326,14 +316,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_token() {
|
fn test_token() {
|
||||||
let result = generate_token(19).unwrap();
|
let result = crypto::generate_token(19).unwrap();
|
||||||
|
|
||||||
assert_eq!(result.chars().count(), 19);
|
assert_eq!(result.chars().count(), 19);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_token_too_large() {
|
fn test_token_too_large() {
|
||||||
let result = generate_token(20);
|
let result = crypto::generate_token(20);
|
||||||
|
|
||||||
assert!(result.is_err(), "too large token should give an error");
|
assert!(result.is_err(), "too large token should give an error");
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
generate_u2f_challenge,
|
generate_u2f_challenge,
|
||||||
activate_u2f,
|
activate_u2f,
|
||||||
activate_u2f_put,
|
activate_u2f_put,
|
||||||
|
delete_u2f,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
|
|||||||
err!("Error registering U2F token")
|
err!("Error registering U2F token")
|
||||||
}
|
}
|
||||||
|
|
||||||
let registration = U2F.register_response(challenge.clone(), response.into())?;
|
let registration = U2F.register_response(challenge, response.into())?;
|
||||||
let full_registration = U2FRegistration {
|
let full_registration = U2FRegistration {
|
||||||
id: data.Id.into_i32()?,
|
id: data.Id.into_i32()?,
|
||||||
name: data.Name,
|
name: data.Name,
|
||||||
@@ -194,6 +195,50 @@ fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbC
|
|||||||
activate_u2f(data, headers, conn)
|
activate_u2f(data, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct DeleteU2FData {
|
||||||
|
Id: NumberOrString,
|
||||||
|
MasterPasswordHash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/two-factor/u2f", data = "<data>")]
|
||||||
|
fn delete_u2f(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
|
let data: DeleteU2FData = data.into_inner().data;
|
||||||
|
|
||||||
|
let id = data.Id.into_i32()?;
|
||||||
|
|
||||||
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
|
err!("Invalid password");
|
||||||
|
}
|
||||||
|
|
||||||
|
let type_ = TwoFactorType::U2f as i32;
|
||||||
|
let mut tf = match TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn) {
|
||||||
|
Some(tf) => tf,
|
||||||
|
None => err!("U2F data not found!"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut data: Vec<U2FRegistration> = match serde_json::from_str(&tf.data) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => err!("Error parsing U2F data"),
|
||||||
|
};
|
||||||
|
|
||||||
|
data.retain(|r| r.id != id);
|
||||||
|
|
||||||
|
let new_data_str = serde_json::to_string(&data)?;
|
||||||
|
|
||||||
|
tf.data = new_data_str;
|
||||||
|
tf.save(&conn)?;
|
||||||
|
|
||||||
|
let keys_json: Vec<Value> = data.iter().map(U2FRegistration::to_json).collect();
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Enabled": true,
|
||||||
|
"Keys": keys_json,
|
||||||
|
"Object": "twoFactorU2f"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
|
fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
|
||||||
let challenge = U2F.generate_challenge().unwrap();
|
let challenge = U2F.generate_challenge().unwrap();
|
||||||
|
|
||||||
|
@@ -16,11 +16,7 @@ use crate::error::{Error, MapResult};
|
|||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![generate_yubikey, activate_yubikey, activate_yubikey_put,]
|
||||||
generate_yubikey,
|
|
||||||
activate_yubikey,
|
|
||||||
activate_yubikey_put,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
@@ -61,20 +61,12 @@ fn icon(domain: String) -> Content<Vec<u8>> {
|
|||||||
return Content(icon_type, FALLBACK_ICON.to_vec());
|
return Content(icon_type, FALLBACK_ICON.to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
if check_icon_domain_is_blacklisted(&domain) {
|
Content(icon_type, get_icon(&domain))
|
||||||
warn!("Domain is blacklisted: {:#?}", domain);
|
|
||||||
return Content(icon_type, FALLBACK_ICON.to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon = get_icon(&domain);
|
|
||||||
|
|
||||||
Content(icon_type, icon)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_icon_domain_is_blacklisted(domain: &str) -> bool {
|
fn check_icon_domain_is_blacklisted(domain: &str) -> bool {
|
||||||
let mut is_blacklisted = false;
|
let mut is_blacklisted = CONFIG.icon_blacklist_non_global_ips()
|
||||||
if CONFIG.icon_blacklist_non_global_ips() {
|
&& (domain, 0)
|
||||||
is_blacklisted = (domain, 0)
|
|
||||||
.to_socket_addrs()
|
.to_socket_addrs()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
for ip_port in x {
|
for ip_port in x {
|
||||||
@@ -86,7 +78,6 @@ fn check_icon_domain_is_blacklisted(domain: &str) -> bool {
|
|||||||
false
|
false
|
||||||
})
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
}
|
|
||||||
|
|
||||||
// Skip the regex check if the previous one is true already
|
// Skip the regex check if the previous one is true already
|
||||||
if !is_blacklisted {
|
if !is_blacklisted {
|
||||||
@@ -121,7 +112,9 @@ fn get_icon(domain: &str) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error downloading icon: {:?}", e);
|
error!("Error downloading icon: {:?}", e);
|
||||||
mark_negcache(&path);
|
let miss_indicator = path + ".miss";
|
||||||
|
let empty_icon = Vec::new();
|
||||||
|
save_icon(&miss_indicator, &empty_icon);
|
||||||
FALLBACK_ICON.to_vec()
|
FALLBACK_ICON.to_vec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,11 +170,6 @@ fn icon_is_negcached(path: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_negcache(path: &str) {
|
|
||||||
let miss_indicator = path.to_owned() + ".miss";
|
|
||||||
File::create(&miss_indicator).expect("Error creating negative cache marker");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon_is_expired(path: &str) -> bool {
|
fn icon_is_expired(path: &str) -> bool {
|
||||||
let expired = file_is_expired(path, CONFIG.icon_cache_ttl());
|
let expired = file_is_expired(path, CONFIG.icon_cache_ttl());
|
||||||
expired.unwrap_or(true)
|
expired.unwrap_or(true)
|
||||||
@@ -225,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(content) = resp {
|
if let Ok(mut 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();
|
||||||
|
|
||||||
@@ -245,12 +233,16 @@ fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
|||||||
// Add the default favicon.ico to the list with the domain the content responded from.
|
// Add the default favicon.ico to the list with the domain the content responded from.
|
||||||
iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
|
iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
|
||||||
|
|
||||||
let soup = Soup::from_reader(content)?;
|
// 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
|
||||||
|
let limited_reader = crate::util::LimitedReader::new(&mut content, 512 * 1024);
|
||||||
|
|
||||||
|
let soup = Soup::from_reader(limited_reader)?;
|
||||||
// Search for and filter
|
// Search for and filter
|
||||||
let favicons = soup
|
let favicons = soup
|
||||||
.tag("link")
|
.tag("link")
|
||||||
.attr("rel", Regex::new(r"icon$|apple.*icon")?) // Only use icon rels
|
.attr("rel", Regex::new(r"icon$|apple.*icon")?) // Only use icon rels
|
||||||
.attr("href", Regex::new(r"(?i)\w+\.(jpg|jpeg|png|ico)(\?.*)?$")?) // Only allow specific extensions
|
.attr("href", Regex::new(r"(?i)\w+\.(jpg|jpeg|png|ico)(\?.*)?$|^data:image.*base64")?) // Only allow specific extensions
|
||||||
.find_all();
|
.find_all();
|
||||||
|
|
||||||
// Loop through all the found icons and determine it's priority
|
// Loop through all the found icons and determine it's priority
|
||||||
@@ -266,6 +258,7 @@ fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
|||||||
} else {
|
} else {
|
||||||
// Add the default favicon.ico to the list with just the given domain
|
// Add the default favicon.ico to the list with just the given domain
|
||||||
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
||||||
|
iconlist.push(Icon::new(35, format!("{}/favicon.ico", httpdomain)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the iconlist by priority
|
// Sort the iconlist by priority
|
||||||
@@ -285,11 +278,7 @@ fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cookie_str.is_empty() {
|
if cookie_str.is_empty() {
|
||||||
CLIENT
|
CLIENT.get(url).send()?.error_for_status().map_err(Into::into)
|
||||||
.get(url)
|
|
||||||
.send()?
|
|
||||||
.error_for_status()
|
|
||||||
.map_err(Into::into)
|
|
||||||
} else {
|
} else {
|
||||||
CLIENT
|
CLIENT
|
||||||
.get(url)
|
.get(url)
|
||||||
@@ -380,19 +369,40 @@ fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
||||||
|
if check_icon_domain_is_blacklisted(domain) {
|
||||||
|
err!("Domain is blacklisted", domain)
|
||||||
|
}
|
||||||
|
|
||||||
let (iconlist, cookie_str) = get_icon_url(&domain)?;
|
let (iconlist, cookie_str) = get_icon_url(&domain)?;
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
use data_url::DataUrl;
|
||||||
|
|
||||||
for icon in iconlist.iter().take(5) {
|
for icon in iconlist.iter().take(5) {
|
||||||
match get_page_with_cookies(&icon.href, &cookie_str) {
|
if icon.href.starts_with("data:image") {
|
||||||
Ok(mut res) => {
|
let datauri = DataUrl::process(&icon.href).unwrap();
|
||||||
info!("Downloaded icon from {}", icon.href);
|
// Check if we are able to decode the data uri
|
||||||
res.copy_to(&mut buffer)?;
|
match datauri.decode_to_vec() {
|
||||||
break;
|
Ok((body, _fragment)) => {
|
||||||
}
|
// Also check if the size is atleast 67 bytes, which seems to be the smallest png i could create
|
||||||
Err(_) => info!("Download failed for {}", icon.href),
|
if body.len() >= 67 {
|
||||||
};
|
buffer = body;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => warn!("data uri is invalid"),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
match get_page_with_cookies(&icon.href, &cookie_str) {
|
||||||
|
Ok(mut res) => {
|
||||||
|
info!("Downloaded icon from {}", icon.href);
|
||||||
|
res.copy_to(&mut buffer)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(_) => info!("Download failed for {}", icon.href),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer.is_empty() {
|
if buffer.is_empty() {
|
||||||
@@ -403,11 +413,17 @@ fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save_icon(path: &str, icon: &[u8]) {
|
fn save_icon(path: &str, icon: &[u8]) {
|
||||||
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache");
|
match File::create(path) {
|
||||||
|
Ok(mut f) => {
|
||||||
if let Ok(mut f) = File::create(path) {
|
f.write_all(icon).expect("Error writing icon file");
|
||||||
f.write_all(icon).expect("Error writing icon file");
|
}
|
||||||
};
|
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
info!("Icon save error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _header_map() -> HeaderMap {
|
fn _header_map() -> HeaderMap {
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
use chrono::Utc;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use rocket::request::{Form, FormItems, FromForm};
|
use rocket::request::{Form, FormItems, FromForm};
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
@@ -96,6 +97,34 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() > CONFIG.signups_verify_resend_time() as i64 {
|
||||||
|
let resend_limit = CONFIG.signups_verify_resend_limit() as i32;
|
||||||
|
if resend_limit == 0 || user.login_verify_count < resend_limit {
|
||||||
|
// We want to send another email verification if we require signups to verify
|
||||||
|
// their email address, and we haven't sent them a reminder in a while...
|
||||||
|
let mut user = user;
|
||||||
|
user.last_verifying_at = Some(now);
|
||||||
|
user.login_verify_count += 1;
|
||||||
|
|
||||||
|
if let Err(e) = user.save(&conn) {
|
||||||
|
error!("Error updating user: {:#?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) {
|
||||||
|
error!("Error auto-sending email verification email: {:#?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We still want the login to fail until they actually verified the email address
|
||||||
|
err!(
|
||||||
|
"Please verify your email before trying again.",
|
||||||
|
format!("IP: {}. Username: {}.", ip.ip, username)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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, &conn)?;
|
||||||
@@ -182,7 +211,7 @@ fn twofactor_auth(
|
|||||||
|
|
||||||
let twofactor_code = match data.two_factor_token {
|
let twofactor_code = match data.two_factor_token {
|
||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA token not provided"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected_twofactor = twofactors
|
let selected_twofactor = twofactors
|
||||||
@@ -197,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(twofactor_code, &selected_data?)?,
|
Some(TwoFactorType::Authenticator) => _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, 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)?,
|
||||||
@@ -208,7 +237,7 @@ fn twofactor_auth(
|
|||||||
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
||||||
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
||||||
}
|
}
|
||||||
_ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
_ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA Remember token not provided"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => err!("Invalid two factor provider"),
|
_ => err!("Invalid two factor provider"),
|
||||||
@@ -293,13 +322,19 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(tf_type @ TwoFactorType::Email) => {
|
Some(tf_type @ TwoFactorType::Email) => {
|
||||||
|
use crate::api::core::two_factor as _tf;
|
||||||
|
|
||||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, &conn) {
|
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, &conn) {
|
||||||
Some(tf) => tf,
|
Some(tf) => tf,
|
||||||
None => err!("No twofactor email registered"),
|
None => err!("No twofactor email registered"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let email_data = EmailTokenData::from_json(&twofactor.data)?;
|
// Send email immediately if email is the only 2FA option
|
||||||
|
if providers.len() == 1 {
|
||||||
|
_tf::email::send_token(&user_uuid, &conn)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let email_data = EmailTokenData::from_json(&twofactor.data)?;
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
"Email": email::obscure_email(&email_data.email),
|
"Email": email::obscure_email(&email_data.email),
|
||||||
})
|
})
|
||||||
|
@@ -1,20 +1,31 @@
|
|||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use crate::api::JsonResult;
|
use crate::api::{EmptyResult, JsonResult};
|
||||||
use crate::auth::Headers;
|
use crate::auth::Headers;
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
|
||||||
use crate::CONFIG;
|
use crate::{Error, CONFIG};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![negotiate, websockets_err]
|
routes![negotiate, websockets_err]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SHOW_WEBSOCKETS_MSG: AtomicBool = AtomicBool::new(true);
|
||||||
|
|
||||||
#[get("/hub")]
|
#[get("/hub")]
|
||||||
fn websockets_err() -> JsonResult {
|
fn websockets_err() -> EmptyResult {
|
||||||
err!("'/notifications/hub' should be proxied to the websocket server or notifications won't work. Go to the README for more info.")
|
if CONFIG.websocket_enabled() && SHOW_WEBSOCKETS_MSG.compare_and_swap(true, false, Ordering::Relaxed) {
|
||||||
|
err!("###########################################################
|
||||||
|
'/notifications/hub' should be proxied to the websocket server or notifications won't work.
|
||||||
|
Go to the Wiki for more info, or disable WebSockets setting WEBSOCKET_ENABLED=false.
|
||||||
|
###########################################################################################")
|
||||||
|
} else {
|
||||||
|
Err(Error::empty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/hub/negotiate")]
|
#[post("/hub/negotiate")]
|
||||||
@@ -43,10 +54,11 @@ fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
|||||||
//
|
//
|
||||||
// Websockets server
|
// Websockets server
|
||||||
//
|
//
|
||||||
|
use std::io;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use ws::{self, util::Token, Factory, Handler, Handshake, Message, Sender, WebSocket};
|
use ws::{self, util::Token, Factory, Handler, Handshake, Message, Sender};
|
||||||
|
|
||||||
use chashmap::CHashMap;
|
use chashmap::CHashMap;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
@@ -124,20 +136,51 @@ struct InitialMessage {
|
|||||||
const PING_MS: u64 = 15_000;
|
const PING_MS: u64 = 15_000;
|
||||||
const PING: Token = Token(1);
|
const PING: Token = Token(1);
|
||||||
|
|
||||||
|
const ID_KEY: &str = "id=";
|
||||||
|
const ACCESS_TOKEN_KEY: &str = "access_token=";
|
||||||
|
|
||||||
|
impl WSHandler {
|
||||||
|
fn err(&self, msg: &'static str) -> ws::Result<()> {
|
||||||
|
self.out.close(ws::CloseCode::Invalid)?;
|
||||||
|
|
||||||
|
// We need to specifically return an IO error so ws closes the connection
|
||||||
|
let io_error = io::Error::from(io::ErrorKind::InvalidData);
|
||||||
|
Err(ws::Error::new(ws::ErrorKind::Io(io_error), msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Handler for WSHandler {
|
impl Handler for WSHandler {
|
||||||
fn on_open(&mut self, hs: Handshake) -> ws::Result<()> {
|
fn on_open(&mut self, hs: Handshake) -> ws::Result<()> {
|
||||||
// TODO: Improve this split
|
// Path == "/notifications/hub?id=<id>==&access_token=<access_token>"
|
||||||
let path = hs.request.resource();
|
let path = hs.request.resource();
|
||||||
let mut query_split: Vec<_> = path.split('?').nth(1).unwrap().split('&').collect();
|
|
||||||
query_split.sort();
|
let (_id, access_token) = match path.split('?').nth(1) {
|
||||||
let access_token = &query_split[0][13..];
|
Some(params) => {
|
||||||
let _id = &query_split[1][3..];
|
let mut params_iter = params.split('&').take(2);
|
||||||
|
|
||||||
|
let mut id = None;
|
||||||
|
let mut access_token = None;
|
||||||
|
while let Some(val) = params_iter.next() {
|
||||||
|
if val.starts_with(ID_KEY) {
|
||||||
|
id = Some(&val[ID_KEY.len()..]);
|
||||||
|
} else if val.starts_with(ACCESS_TOKEN_KEY) {
|
||||||
|
access_token = Some(&val[ACCESS_TOKEN_KEY.len()..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match (id, access_token) {
|
||||||
|
(Some(a), Some(b)) => (a, b),
|
||||||
|
_ => return self.err("Missing id or access token"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return self.err("Missing query path"),
|
||||||
|
};
|
||||||
|
|
||||||
// Validate the user
|
// Validate the user
|
||||||
use crate::auth;
|
use crate::auth;
|
||||||
let claims = match auth::decode_login(access_token) {
|
let claims = match auth::decode_login(access_token) {
|
||||||
Ok(claims) => claims,
|
Ok(claims) => claims,
|
||||||
Err(_) => return Err(ws::Error::new(ws::ErrorKind::Internal, "Invalid access token provided")),
|
Err(_) => return self.err("Invalid access token provided"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Assign the user to the handler
|
// Assign the user to the handler
|
||||||
@@ -157,8 +200,6 @@ impl Handler for WSHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_message(&mut self, msg: Message) -> ws::Result<()> {
|
fn on_message(&mut self, msg: Message) -> ws::Result<()> {
|
||||||
info!("Server got message '{}'. ", msg);
|
|
||||||
|
|
||||||
if let Message::Text(text) = msg.clone() {
|
if let Message::Text(text) = msg.clone() {
|
||||||
let json = &text[..text.len() - 1]; // Remove last char
|
let json = &text[..text.len() - 1]; // Remove last char
|
||||||
|
|
||||||
@@ -181,10 +222,7 @@ impl Handler for WSHandler {
|
|||||||
// reschedule the timeout
|
// reschedule the timeout
|
||||||
self.out.timeout(PING_MS, PING)
|
self.out.timeout(PING_MS, PING)
|
||||||
} else {
|
} else {
|
||||||
Err(ws::Error::new(
|
Ok(())
|
||||||
ws::ErrorKind::Internal,
|
|
||||||
"Invalid timeout token provided",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,7 +391,14 @@ pub fn start_notification_server() -> WebSocketUsers {
|
|||||||
|
|
||||||
if CONFIG.websocket_enabled() {
|
if CONFIG.websocket_enabled() {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
WebSocket::new(factory)
|
let mut settings = ws::Settings::default();
|
||||||
|
settings.max_connections = 500;
|
||||||
|
settings.queue_size = 2;
|
||||||
|
settings.panic_on_internal = false;
|
||||||
|
|
||||||
|
ws::Builder::new()
|
||||||
|
.with_settings(settings)
|
||||||
|
.build(factory)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.listen((CONFIG.websocket_address().as_str(), CONFIG.websocket_port()))
|
.listen((CONFIG.websocket_address().as_str(), CONFIG.websocket_port()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@@ -7,11 +7,13 @@ use rocket::Route;
|
|||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::util::Cached;
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
use crate::util::Cached;
|
||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
|
// If addding more routes here, consider also adding them to
|
||||||
|
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
||||||
if CONFIG.web_vault_enabled() {
|
if CONFIG.web_vault_enabled() {
|
||||||
routes![web_index, app_id, web_files, attachments, alive, static_files]
|
routes![web_index, app_id, web_files, attachments, alive, static_files]
|
||||||
} else {
|
} else {
|
||||||
@@ -21,9 +23,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
fn web_index() -> Cached<Option<NamedFile>> {
|
fn web_index() -> Cached<Option<NamedFile>> {
|
||||||
Cached::short(NamedFile::open(
|
Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).ok())
|
||||||
Path::new(&CONFIG.web_vault_folder()).join("index.html"),
|
|
||||||
).ok())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/app-id.json")]
|
#[get("/app-id.json")]
|
||||||
@@ -69,6 +69,7 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> {
|
|||||||
"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"))),
|
||||||
"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"))),
|
||||||
|
|
||||||
"bootstrap.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
"bootstrap.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
||||||
"bootstrap-native-v4.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native-v4.js"))),
|
"bootstrap-native-v4.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native-v4.js"))),
|
||||||
|
87
src/auth.rs
87
src/auth.rs
@@ -18,6 +18,8 @@ lazy_static! {
|
|||||||
static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM);
|
static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM);
|
||||||
pub static ref JWT_LOGIN_ISSUER: String = format!("{}|login", CONFIG.domain());
|
pub static ref JWT_LOGIN_ISSUER: String = format!("{}|login", CONFIG.domain());
|
||||||
pub static ref JWT_INVITE_ISSUER: String = format!("{}|invite", CONFIG.domain());
|
pub static ref JWT_INVITE_ISSUER: String = format!("{}|invite", CONFIG.domain());
|
||||||
|
pub static ref JWT_DELETE_ISSUER: String = format!("{}|delete", CONFIG.domain());
|
||||||
|
pub static ref JWT_VERIFYEMAIL_ISSUER: String = format!("{}|verifyemail", CONFIG.domain());
|
||||||
pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain());
|
pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain());
|
||||||
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key()) {
|
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key()) {
|
||||||
Ok(key) => key,
|
Ok(key) => key,
|
||||||
@@ -62,6 +64,14 @@ pub fn decode_invite(token: &str) -> Result<InviteJWTClaims, Error> {
|
|||||||
decode_jwt(token, JWT_INVITE_ISSUER.to_string())
|
decode_jwt(token, JWT_INVITE_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn decode_delete(token: &str) -> Result<DeleteJWTClaims, Error> {
|
||||||
|
decode_jwt(token, JWT_DELETE_ISSUER.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_verify_email(token: &str) -> Result<VerifyEmailJWTClaims, Error> {
|
||||||
|
decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn decode_admin(token: &str) -> Result<AdminJWTClaims, Error> {
|
pub fn decode_admin(token: &str) -> Result<AdminJWTClaims, Error> {
|
||||||
decode_jwt(token, JWT_ADMIN_ISSUER.to_string())
|
decode_jwt(token, JWT_ADMIN_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
@@ -118,7 +128,7 @@ pub fn generate_invite_claims(
|
|||||||
uuid: String,
|
uuid: String,
|
||||||
email: String,
|
email: String,
|
||||||
org_id: Option<String>,
|
org_id: Option<String>,
|
||||||
org_user_id: Option<String>,
|
user_org_id: Option<String>,
|
||||||
invited_by_email: Option<String>,
|
invited_by_email: Option<String>,
|
||||||
) -> InviteJWTClaims {
|
) -> InviteJWTClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
@@ -126,11 +136,55 @@ pub fn generate_invite_claims(
|
|||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + Duration::days(5)).timestamp(),
|
exp: (time_now + Duration::days(5)).timestamp(),
|
||||||
iss: JWT_INVITE_ISSUER.to_string(),
|
iss: JWT_INVITE_ISSUER.to_string(),
|
||||||
sub: uuid.clone(),
|
sub: uuid,
|
||||||
email: email.clone(),
|
email,
|
||||||
org_id: org_id.clone(),
|
org_id,
|
||||||
user_org_id: org_user_id.clone(),
|
user_org_id,
|
||||||
invited_by_email: invited_by_email.clone(),
|
invited_by_email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct DeleteJWTClaims {
|
||||||
|
// Not before
|
||||||
|
pub nbf: i64,
|
||||||
|
// Expiration time
|
||||||
|
pub exp: i64,
|
||||||
|
// Issuer
|
||||||
|
pub iss: String,
|
||||||
|
// Subject
|
||||||
|
pub sub: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_delete_claims(uuid: String) -> DeleteJWTClaims {
|
||||||
|
let time_now = Utc::now().naive_utc();
|
||||||
|
DeleteJWTClaims {
|
||||||
|
nbf: time_now.timestamp(),
|
||||||
|
exp: (time_now + Duration::days(5)).timestamp(),
|
||||||
|
iss: JWT_DELETE_ISSUER.to_string(),
|
||||||
|
sub: uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct VerifyEmailJWTClaims {
|
||||||
|
// Not before
|
||||||
|
pub nbf: i64,
|
||||||
|
// Expiration time
|
||||||
|
pub exp: i64,
|
||||||
|
// Issuer
|
||||||
|
pub iss: String,
|
||||||
|
// Subject
|
||||||
|
pub sub: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_verify_email_claims(uuid: String) -> DeleteJWTClaims {
|
||||||
|
let time_now = Utc::now().naive_utc();
|
||||||
|
DeleteJWTClaims {
|
||||||
|
nbf: time_now.timestamp(),
|
||||||
|
exp: (time_now + Duration::days(5)).timestamp(),
|
||||||
|
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
|
||||||
|
sub: uuid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,12 +426,25 @@ pub struct ClientIp {
|
|||||||
impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
|
impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
fn from_request(req: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||||
let ip = match request.client_ip() {
|
let ip = if CONFIG._ip_header_enabled() {
|
||||||
Some(addr) => addr,
|
req.headers().get_one(&CONFIG.ip_header()).and_then(|ip| {
|
||||||
None => "0.0.0.0".parse().unwrap(),
|
match ip.find(',') {
|
||||||
|
Some(idx) => &ip[..idx],
|
||||||
|
None => ip,
|
||||||
|
}
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| warn!("'{}' header is malformed: {}", CONFIG.ip_header(), ip))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ip = ip
|
||||||
|
.or_else(|| req.remote().map(|r| r.ip()))
|
||||||
|
.unwrap_or_else(|| "0.0.0.0".parse().unwrap());
|
||||||
|
|
||||||
Outcome::Success(ClientIp { ip })
|
Outcome::Success(ClientIp { ip })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
105
src/config.rs
105
src/config.rs
@@ -185,19 +185,24 @@ macro_rules! make_config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
|
( @build $value:expr, $config:expr, gen, $default_fn:expr ) => {{
|
||||||
|
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
|
||||||
|
f($config)
|
||||||
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
//STRUCTURE:
|
//STRUCTURE:
|
||||||
// /// Short description (without this they won't appear on the list)
|
// /// Short description (without this they won't appear on the list)
|
||||||
// group {
|
// group {
|
||||||
// /// Friendly Name |> Description (Optional)
|
// /// Friendly Name |> Description (Optional)
|
||||||
// name: type, is_editable, none_action, <default_value (Optional)>
|
// name: type, is_editable, action, <default_value (Optional)>
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// Where none_action applied when the value wasn't provided and can be:
|
// Where action applied when the value wasn't provided and can be:
|
||||||
// def: Use a default value
|
// def: Use a default value
|
||||||
// auto: Value is auto generated based on other values
|
// auto: Value is auto generated based on other values
|
||||||
// option: Value is optional
|
// option: Value is optional
|
||||||
|
// gen: Value is always autogenerated and it's original value ignored
|
||||||
make_config! {
|
make_config! {
|
||||||
folders {
|
folders {
|
||||||
/// Data folder |> Main data folder
|
/// Data folder |> Main data folder
|
||||||
@@ -243,6 +248,14 @@ make_config! {
|
|||||||
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 if new users can register. Note that while this is disabled, users could still be invited
|
||||||
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
|
||||||
|
signups_verify: bool, true, def, false;
|
||||||
|
/// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
|
||||||
|
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)
|
||||||
|
signups_verify_resend_limit: u32, true, def, 6;
|
||||||
|
/// Allow signups only from this list of comma-separated domains
|
||||||
|
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 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.
|
||||||
@@ -258,6 +271,11 @@ make_config! {
|
|||||||
|
|
||||||
/// Advanced settings
|
/// Advanced settings
|
||||||
advanced {
|
advanced {
|
||||||
|
/// Client IP header |> If not present, the remote IP is used.
|
||||||
|
/// Set to the string "none" (without quotes), to disable any headers and just use the remote IP
|
||||||
|
ip_header: String, true, def, "X-Real-IP".to_string();
|
||||||
|
/// Internal IP header property, used to avoid recomputing each time
|
||||||
|
_ip_header_enabled: bool, false, gen, |c| &c.ip_header.trim().to_lowercase() != "none";
|
||||||
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
|
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
|
||||||
icon_cache_ttl: u64, true, def, 2_592_000;
|
icon_cache_ttl: u64, true, def, 2_592_000;
|
||||||
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
|
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
|
||||||
@@ -275,6 +293,10 @@ make_config! {
|
|||||||
/// Note that the checkbox would still be present, but ignored.
|
/// Note that the checkbox would still be present, but ignored.
|
||||||
disable_2fa_remember: bool, true, def, false;
|
disable_2fa_remember: bool, true, def, false;
|
||||||
|
|
||||||
|
/// Disable authenticator time drifted codes to be valid |> Enabling this only allows the current TOTP code to be valid
|
||||||
|
/// TOTP codes of the previous and next 30 seconds will be invalid.
|
||||||
|
authenticator_disable_time_drift: bool, true, def, false;
|
||||||
|
|
||||||
/// Require new device emails |> When a user logs in an email is required to be sent.
|
/// Require new device emails |> When a user logs in an email is required to be sent.
|
||||||
/// If sending the email fails the login attempt will fail.
|
/// If sending the email fails the login attempt will fail.
|
||||||
require_device_email: bool, true, def, false;
|
require_device_email: bool, true, def, false;
|
||||||
@@ -282,9 +304,6 @@ make_config! {
|
|||||||
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
|
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
|
||||||
/// ONLY use this during development, as it can slow down the server
|
/// ONLY use this during development, as it can slow down the server
|
||||||
reload_templates: bool, true, def, false;
|
reload_templates: bool, true, def, false;
|
||||||
|
|
||||||
/// Log routes at launch (Dev)
|
|
||||||
log_mounts: bool, true, def, false;
|
|
||||||
/// Enable extended logging
|
/// Enable extended logging
|
||||||
extended_logging: bool, false, def, true;
|
extended_logging: bool, false, def, true;
|
||||||
/// Enable the log to output to Syslog
|
/// Enable the log to output to Syslog
|
||||||
@@ -298,7 +317,7 @@ make_config! {
|
|||||||
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
||||||
enable_db_wal: bool, false, def, true;
|
enable_db_wal: bool, false, def, true;
|
||||||
|
|
||||||
/// Disable Admin Token (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
/// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
||||||
disable_admin_token: bool, true, def, false;
|
disable_admin_token: bool, true, def, false;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -328,18 +347,6 @@ make_config! {
|
|||||||
_duo_akey: Pass, false, option;
|
_duo_akey: Pass, false, option;
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Email 2FA Settings
|
|
||||||
email_2fa: _enable_email_2fa {
|
|
||||||
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
|
|
||||||
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && c.smtp_host.is_some();
|
|
||||||
/// Token number length |> Length of the numbers in an email token. Minimum of 6. Maximum is 19.
|
|
||||||
email_token_size: u32, true, def, 6;
|
|
||||||
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
|
||||||
email_expiration_time: u64, true, def, 600;
|
|
||||||
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
|
|
||||||
email_attempts_limit: u64, true, def, 3;
|
|
||||||
},
|
|
||||||
|
|
||||||
/// SMTP Email Settings
|
/// SMTP Email Settings
|
||||||
smtp: _enable_smtp {
|
smtp: _enable_smtp {
|
||||||
/// Enabled
|
/// Enabled
|
||||||
@@ -362,10 +369,37 @@ make_config! {
|
|||||||
smtp_password: Pass, true, option;
|
smtp_password: Pass, true, option;
|
||||||
/// Json form auth mechanism |> Defaults for ssl is "Plain" and "Login" and nothing for non-ssl connections. Possible values: ["Plain", "Login", "Xoauth2"]
|
/// Json form auth mechanism |> Defaults for ssl is "Plain" and "Login" and nothing for non-ssl connections. Possible values: ["Plain", "Login", "Xoauth2"]
|
||||||
smtp_auth_mechanism: String, true, option;
|
smtp_auth_mechanism: String, true, option;
|
||||||
|
/// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
|
||||||
|
smtp_timeout: u64, true, def, 15;
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Email 2FA Settings
|
||||||
|
email_2fa: _enable_email_2fa {
|
||||||
|
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
|
||||||
|
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && c.smtp_host.is_some();
|
||||||
|
/// Token number length |> Length of the numbers in an email token. Minimum of 6. Maximum is 19.
|
||||||
|
email_token_size: u32, true, def, 6;
|
||||||
|
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
||||||
|
email_expiration_time: u64, true, def, 600;
|
||||||
|
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
|
||||||
|
email_attempts_limit: u64, true, def, 3;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
|
let db_url = cfg.database_url.to_lowercase();
|
||||||
|
if cfg!(feature = "sqlite") && (db_url.starts_with("mysql:") || db_url.starts_with("postgresql:")) {
|
||||||
|
err!("`DATABASE_URL` is meant for MySQL or Postgres, while this server is meant for SQLite")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(feature = "mysql") && !db_url.starts_with("mysql:") {
|
||||||
|
err!("`DATABASE_URL` should start with mysql: when using the MySQL server")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(feature = "postgresql") && !db_url.starts_with("postgresql:") {
|
||||||
|
err!("`DATABASE_URL` should start with postgresql: when using the PostgreSQL server")
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(ref token) = cfg.admin_token {
|
if let Some(ref token) = cfg.admin_token {
|
||||||
if token.trim().is_empty() {
|
if token.trim().is_empty() {
|
||||||
err!("`ADMIN_TOKEN` is enabled but has an empty value. To enable the admin page without token, use `DISABLE_ADMIN_TOKEN`")
|
err!("`ADMIN_TOKEN` is enabled but has an empty value. To enable the admin page without token, use `DISABLE_ADMIN_TOKEN`")
|
||||||
@@ -422,12 +456,7 @@ impl Config {
|
|||||||
validate_config(&config)?;
|
validate_config(&config)?;
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
inner: RwLock::new(Inner {
|
inner: RwLock::new(Inner { templates: load_templates(&config.templates_folder), config, _env, _usr }),
|
||||||
templates: load_templates(&config.templates_folder),
|
|
||||||
config,
|
|
||||||
_env,
|
|
||||||
_usr,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,6 +500,15 @@ impl Config {
|
|||||||
self.update_config(builder)
|
self.update_config(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn can_signup_user(&self, email: &str) -> bool {
|
||||||
|
let e: Vec<&str> = email.rsplitn(2, '@').collect();
|
||||||
|
if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {
|
||||||
|
warn!("Failed to parse email address '{}'", email);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.signups_domains_whitelist().split(',').any(|d| d == e[0])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn delete_user_config(&self) -> Result<(), Error> {
|
pub fn delete_user_config(&self) -> Result<(), Error> {
|
||||||
crate::util::delete_file(&CONFIG_FILE)?;
|
crate::util::delete_file(&CONFIG_FILE)?;
|
||||||
|
|
||||||
@@ -563,6 +601,8 @@ fn load_templates(path: &str) -> Handlebars {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First register default templates here
|
// First register default templates here
|
||||||
|
reg!("email/change_email", ".html");
|
||||||
|
reg!("email/delete_account", ".html");
|
||||||
reg!("email/invite_accepted", ".html");
|
reg!("email/invite_accepted", ".html");
|
||||||
reg!("email/invite_confirmed", ".html");
|
reg!("email/invite_confirmed", ".html");
|
||||||
reg!("email/new_device_logged_in", ".html");
|
reg!("email/new_device_logged_in", ".html");
|
||||||
@@ -570,6 +610,9 @@ fn load_templates(path: &str) -> Handlebars {
|
|||||||
reg!("email/pw_hint_some", ".html");
|
reg!("email/pw_hint_some", ".html");
|
||||||
reg!("email/send_org_invite", ".html");
|
reg!("email/send_org_invite", ".html");
|
||||||
reg!("email/twofactor_email", ".html");
|
reg!("email/twofactor_email", ".html");
|
||||||
|
reg!("email/verify_email", ".html");
|
||||||
|
reg!("email/welcome", ".html");
|
||||||
|
reg!("email/welcome_must_verify", ".html");
|
||||||
|
|
||||||
reg!("admin/base");
|
reg!("admin/base");
|
||||||
reg!("admin/login");
|
reg!("admin/login");
|
||||||
@@ -594,9 +637,7 @@ impl HelperDef for CaseHelper {
|
|||||||
rc: &mut RenderContext<'reg>,
|
rc: &mut RenderContext<'reg>,
|
||||||
out: &mut dyn Output,
|
out: &mut dyn Output,
|
||||||
) -> HelperResult {
|
) -> HelperResult {
|
||||||
let param = h
|
let param = h.param(0).ok_or_else(|| RenderError::new("Param not found for helper \"case\""))?;
|
||||||
.param(0)
|
|
||||||
.ok_or_else(|| RenderError::new("Param not found for helper \"case\""))?;
|
|
||||||
let value = param.value().clone();
|
let value = param.value().clone();
|
||||||
|
|
||||||
if h.params().iter().skip(1).any(|x| x.value() == &value) {
|
if h.params().iter().skip(1).any(|x| x.value() == &value) {
|
||||||
@@ -618,14 +659,10 @@ impl HelperDef for JsEscapeHelper {
|
|||||||
_: &mut RenderContext<'reg>,
|
_: &mut RenderContext<'reg>,
|
||||||
out: &mut dyn Output,
|
out: &mut dyn Output,
|
||||||
) -> HelperResult {
|
) -> HelperResult {
|
||||||
let param = h
|
let param = h.param(0).ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?;
|
||||||
.param(0)
|
|
||||||
.ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?;
|
|
||||||
|
|
||||||
let value = param
|
let value =
|
||||||
.value()
|
param.value().as_str().ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?;
|
||||||
.as_str()
|
|
||||||
.ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?;
|
|
||||||
|
|
||||||
let escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
|
let escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
|
||||||
let quoted_value = format!(""{}"", escaped_value);
|
let quoted_value = format!(""{}"", escaped_value);
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
// PBKDF2 derivation
|
// PBKDF2 derivation
|
||||||
//
|
//
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
use ring::{digest, hmac, pbkdf2};
|
use ring::{digest, hmac, pbkdf2};
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
@@ -52,6 +53,21 @@ pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
|
|||||||
array
|
array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_token(token_size: u32) -> Result<String, Error> {
|
||||||
|
if token_size > 19 {
|
||||||
|
err!("Generating token failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8 bytes to create an u64 for up to 19 token digits
|
||||||
|
let bytes = get_random(vec![0; 8]);
|
||||||
|
let mut bytes_array = [0u8; 8];
|
||||||
|
bytes_array.copy_from_slice(&bytes);
|
||||||
|
|
||||||
|
let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size);
|
||||||
|
let token = format!("{:0size$}", number, size = token_size as usize);
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Constant time compare
|
// Constant time compare
|
||||||
//
|
//
|
||||||
|
@@ -52,12 +52,16 @@ pub fn get_connection() -> Result<Connection, ConnectionError> {
|
|||||||
|
|
||||||
/// Creates a back-up of the database using sqlite3
|
/// Creates a back-up of the database using sqlite3
|
||||||
pub fn backup_database() -> Result<(), Error> {
|
pub fn backup_database() -> Result<(), Error> {
|
||||||
|
use std::path::Path;
|
||||||
|
let db_url = CONFIG.database_url();
|
||||||
|
let db_path = Path::new(&db_url).parent().unwrap();
|
||||||
|
|
||||||
let now: DateTime<Utc> = Utc::now();
|
let now: DateTime<Utc> = Utc::now();
|
||||||
let file_date = now.format("%Y%m%d").to_string();
|
let file_date = now.format("%Y%m%d").to_string();
|
||||||
let backup_command: String = format!("{}{}{}", ".backup 'db_", file_date, ".sqlite3'");
|
let backup_command: String = format!("{}{}{}", ".backup 'db_", file_date, ".sqlite3'");
|
||||||
|
|
||||||
Command::new("sqlite3")
|
Command::new("sqlite3")
|
||||||
.current_dir("./data")
|
.current_dir(db_path)
|
||||||
.args(&["db.sqlite3", &backup_command])
|
.args(&["db.sqlite3", &backup_command])
|
||||||
.output()
|
.output()
|
||||||
.expect("Can't open database, sqlite3 is not available, make sure it's installed and available on the PATH");
|
.expect("Can't open database, sqlite3 is not available, make sure it's installed and available on the PATH");
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
|
||||||
use super::User;
|
use super::User;
|
||||||
|
use crate::CONFIG;
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "devices"]
|
#[table_name = "devices"]
|
||||||
@@ -87,7 +88,7 @@ impl Device {
|
|||||||
premium: true,
|
premium: true,
|
||||||
name: user.name.to_string(),
|
name: user.name.to_string(),
|
||||||
email: user.email.to_string(),
|
email: user.email.to_string(),
|
||||||
email_verified: true,
|
email_verified: !CONFIG.mail_enabled() || user.verified_at.is_some(),
|
||||||
|
|
||||||
orgowner,
|
orgowner,
|
||||||
orgadmin,
|
orgadmin,
|
||||||
|
@@ -19,6 +19,7 @@ pub struct TwoFactor {
|
|||||||
pub atype: i32,
|
pub atype: i32,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub data: String,
|
pub data: String,
|
||||||
|
pub last_used: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -47,6 +48,7 @@ impl TwoFactor {
|
|||||||
atype: atype as i32,
|
atype: atype as i32,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
data,
|
data,
|
||||||
|
last_used: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,8 +11,13 @@ pub struct User {
|
|||||||
pub uuid: String,
|
pub uuid: String,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
|
pub verified_at: Option<NaiveDateTime>,
|
||||||
|
pub last_verifying_at: Option<NaiveDateTime>,
|
||||||
|
pub login_verify_count: i32,
|
||||||
|
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
pub email_new: Option<String>,
|
||||||
|
pub email_new_token: Option<String>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
pub password_hash: Vec<u8>,
|
pub password_hash: Vec<u8>,
|
||||||
@@ -56,9 +61,14 @@ impl User {
|
|||||||
uuid: crate::util::get_uuid(),
|
uuid: crate::util::get_uuid(),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
|
verified_at: None,
|
||||||
|
last_verifying_at: None,
|
||||||
|
login_verify_count: 0,
|
||||||
name: email.clone(),
|
name: email.clone(),
|
||||||
email,
|
email,
|
||||||
akey: String::new(),
|
akey: String::new(),
|
||||||
|
email_new: None,
|
||||||
|
email_new_token: None,
|
||||||
|
|
||||||
password_hash: Vec::new(),
|
password_hash: Vec::new(),
|
||||||
salt: crypto::get_random_64(),
|
salt: crypto::get_random_64(),
|
||||||
@@ -135,7 +145,7 @@ impl User {
|
|||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"Name": self.name,
|
"Name": self.name,
|
||||||
"Email": self.email,
|
"Email": self.email,
|
||||||
"EmailVerified": true,
|
"EmailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(),
|
||||||
"Premium": true,
|
"Premium": true,
|
||||||
"MasterPasswordHint": self.password_hint,
|
"MasterPasswordHint": self.password_hint,
|
||||||
"Culture": "en-US",
|
"Culture": "en-US",
|
||||||
|
@@ -92,6 +92,7 @@ table! {
|
|||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
enabled -> Bool,
|
enabled -> Bool,
|
||||||
data -> Text,
|
data -> Text,
|
||||||
|
last_used -> Integer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,12 @@ table! {
|
|||||||
uuid -> Varchar,
|
uuid -> Varchar,
|
||||||
created_at -> Datetime,
|
created_at -> Datetime,
|
||||||
updated_at -> Datetime,
|
updated_at -> Datetime,
|
||||||
|
verified_at -> Nullable<Datetime>,
|
||||||
|
last_verifying_at -> Nullable<Datetime>,
|
||||||
|
login_verify_count -> Integer,
|
||||||
email -> Varchar,
|
email -> Varchar,
|
||||||
|
email_new -> Nullable<Varchar>,
|
||||||
|
email_new_token -> Nullable<Varchar>,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
password_hash -> Blob,
|
password_hash -> Blob,
|
||||||
salt -> Blob,
|
salt -> Blob,
|
||||||
|
@@ -92,6 +92,7 @@ table! {
|
|||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
enabled -> Bool,
|
enabled -> Bool,
|
||||||
data -> Text,
|
data -> Text,
|
||||||
|
last_used -> Integer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,12 @@ table! {
|
|||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Timestamp,
|
||||||
|
verified_at -> Nullable<Timestamp>,
|
||||||
|
last_verifying_at -> Nullable<Timestamp>,
|
||||||
|
login_verify_count -> Integer,
|
||||||
email -> Text,
|
email -> Text,
|
||||||
|
email_new -> Nullable<Text>,
|
||||||
|
email_new_token -> Nullable<Text>,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
password_hash -> Binary,
|
password_hash -> Binary,
|
||||||
salt -> Binary,
|
salt -> Binary,
|
||||||
|
@@ -92,6 +92,7 @@ table! {
|
|||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
enabled -> Bool,
|
enabled -> Bool,
|
||||||
data -> Text,
|
data -> Text,
|
||||||
|
last_used -> Integer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,12 @@ table! {
|
|||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Timestamp,
|
||||||
|
verified_at -> Nullable<Timestamp>,
|
||||||
|
last_verifying_at -> Nullable<Timestamp>,
|
||||||
|
login_verify_count -> Integer,
|
||||||
email -> Text,
|
email -> Text,
|
||||||
|
email_new -> Nullable<Text>,
|
||||||
|
email_new_token -> Nullable<Text>,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
password_hash -> Binary,
|
password_hash -> Binary,
|
||||||
salt -> Binary,
|
salt -> Binary,
|
||||||
|
29
src/error.rs
29
src/error.rs
@@ -86,7 +86,18 @@ impl std::fmt::Debug for Error {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self.source() {
|
match self.source() {
|
||||||
Some(e) => write!(f, "{}.\n[CAUSE] {:#?}", self.message, e),
|
Some(e) => write!(f, "{}.\n[CAUSE] {:#?}", self.message, e),
|
||||||
None => write!(f, "{}. {}", self.message, self.error),
|
None => match self.error {
|
||||||
|
ErrorKind::EmptyError(_) => Ok(()),
|
||||||
|
ErrorKind::SimpleError(ref s) => {
|
||||||
|
if &self.message == s {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}. {}", self.message, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrorKind::JsonError(_) => write!(f, "{}", self.message),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,15 +181,17 @@ use rocket::response::{self, Responder, Response};
|
|||||||
|
|
||||||
impl<'r> Responder<'r> for Error {
|
impl<'r> Responder<'r> for Error {
|
||||||
fn respond_to(self, _: &Request) -> response::Result<'r> {
|
fn respond_to(self, _: &Request) -> response::Result<'r> {
|
||||||
let usr_msg = format!("{}", self);
|
match self.error {
|
||||||
error!("{:#?}", self);
|
ErrorKind::EmptyError(_) => {} // Don't print the error in this situation
|
||||||
|
_ => error!(target: "error", "{:#?}", self),
|
||||||
|
};
|
||||||
|
|
||||||
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
|
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
|
||||||
|
|
||||||
Response::build()
|
Response::build()
|
||||||
.status(code)
|
.status(code)
|
||||||
.header(ContentType::JSON)
|
.header(ContentType::JSON)
|
||||||
.sized_body(Cursor::new(usr_msg))
|
.sized_body(Cursor::new(format!("{}", self)))
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,19 +211,19 @@ macro_rules! err {
|
|||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! err_json {
|
macro_rules! err_json {
|
||||||
($expr:expr) => {{
|
($expr:expr, $log_value:expr) => {{
|
||||||
return Err(crate::error::Error::from($expr));
|
return Err(($log_value, $expr).into());
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! err_handler {
|
macro_rules! err_handler {
|
||||||
($expr:expr) => {{
|
($expr:expr) => {{
|
||||||
error!("Unauthorized Error: {}", $expr);
|
error!(target: "auth", "Unauthorized Error: {}", $expr);
|
||||||
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
|
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
|
||||||
}};
|
}};
|
||||||
($usr_msg:expr, $log_value:expr) => {{
|
($usr_msg:expr, $log_value:expr) => {{
|
||||||
error!("Unauthorized Error: {}. {}", $usr_msg, $log_value);
|
error!(target: "auth", "Unauthorized Error: {}. {}", $usr_msg, $log_value);
|
||||||
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $usr_msg));
|
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $usr_msg));
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
80
src/mail.rs
80
src/mail.rs
@@ -8,7 +8,7 @@ use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
|||||||
use quoted_printable::encode_to_str;
|
use quoted_printable::encode_to_str;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::auth::{encode_jwt, generate_invite_claims};
|
use crate::auth::{encode_jwt, generate_delete_claims, generate_invite_claims, generate_verify_email_claims};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
@@ -33,6 +33,8 @@ fn mailer() -> SmtpTransport {
|
|||||||
ClientSecurity::None
|
ClientSecurity::None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
let smtp_client = SmtpClient::new((host.as_str(), CONFIG.smtp_port()), client_security).unwrap();
|
let smtp_client = SmtpClient::new((host.as_str(), CONFIG.smtp_port()), client_security).unwrap();
|
||||||
|
|
||||||
let smtp_client = match (&CONFIG.smtp_username(), &CONFIG.smtp_password()) {
|
let smtp_client = match (&CONFIG.smtp_username(), &CONFIG.smtp_password()) {
|
||||||
@@ -53,6 +55,7 @@ fn mailer() -> SmtpTransport {
|
|||||||
|
|
||||||
smtp_client
|
smtp_client
|
||||||
.smtp_utf8(true)
|
.smtp_utf8(true)
|
||||||
|
.timeout(Some(Duration::from_secs(CONFIG.smtp_timeout())))
|
||||||
.connection_reuse(ConnectionReuseParameters::NoReuse)
|
.connection_reuse(ConnectionReuseParameters::NoReuse)
|
||||||
.transport()
|
.transport()
|
||||||
}
|
}
|
||||||
@@ -92,6 +95,67 @@ pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
|
|||||||
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 {
|
||||||
|
let claims = generate_delete_claims(uuid.to_string());
|
||||||
|
let delete_token = encode_jwt(&claims);
|
||||||
|
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/delete_account",
|
||||||
|
json!({
|
||||||
|
"url": CONFIG.domain(),
|
||||||
|
"user_id": uuid,
|
||||||
|
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
||||||
|
"token": delete_token,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_email(&address, &subject, &body_html, &body_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
||||||
|
let claims = generate_verify_email_claims(uuid.to_string());
|
||||||
|
let verify_email_token = encode_jwt(&claims);
|
||||||
|
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/verify_email",
|
||||||
|
json!({
|
||||||
|
"url": CONFIG.domain(),
|
||||||
|
"user_id": uuid,
|
||||||
|
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
||||||
|
"token": verify_email_token,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_email(&address, &subject, &body_html, &body_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_welcome(address: &str) -> EmptyResult {
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/welcome",
|
||||||
|
json!({
|
||||||
|
"url": CONFIG.domain(),
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_email(&address, &subject, &body_html, &body_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
||||||
|
let claims = generate_verify_email_claims(uuid.to_string());
|
||||||
|
let verify_email_token = encode_jwt(&claims);
|
||||||
|
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/welcome_must_verify",
|
||||||
|
json!({
|
||||||
|
"url": CONFIG.domain(),
|
||||||
|
"user_id": uuid,
|
||||||
|
"token": verify_email_token,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_email(&address, &subject, &body_html, &body_text)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_invite(
|
pub fn send_invite(
|
||||||
address: &str,
|
address: &str,
|
||||||
uuid: &str,
|
uuid: &str,
|
||||||
@@ -105,7 +169,7 @@ pub fn send_invite(
|
|||||||
String::from(address),
|
String::from(address),
|
||||||
org_id.clone(),
|
org_id.clone(),
|
||||||
org_user_id.clone(),
|
org_user_id.clone(),
|
||||||
invited_by_email.clone(),
|
invited_by_email,
|
||||||
);
|
);
|
||||||
let invite_token = encode_jwt(&claims);
|
let invite_token = encode_jwt(&claims);
|
||||||
|
|
||||||
@@ -180,6 +244,18 @@ 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 {
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/change_email",
|
||||||
|
json!({
|
||||||
|
"url": CONFIG.domain(),
|
||||||
|
"token": token,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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 {
|
||||||
let html = PartBuilder::new()
|
let html = PartBuilder::new()
|
||||||
.body(encode_to_str(body_html))
|
.body(encode_to_str(body_html))
|
||||||
|
84
src/main.rs
84
src/main.rs
@@ -1,4 +1,4 @@
|
|||||||
#![feature(proc_macro_hygiene, decl_macro, vec_remove_item, try_trait, ip)]
|
#![feature(proc_macro_hygiene, vec_remove_item, try_trait, ip)]
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
@@ -23,8 +23,10 @@ extern crate derive_more;
|
|||||||
extern crate num_derive;
|
extern crate num_derive;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
fs::create_dir_all,
|
||||||
path::Path,
|
path::Path,
|
||||||
process::{exit, Command},
|
process::{exit, Command},
|
||||||
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@@ -43,16 +45,23 @@ pub use error::{Error, MapResult};
|
|||||||
fn main() {
|
fn main() {
|
||||||
launch_info();
|
launch_info();
|
||||||
|
|
||||||
if CONFIG.extended_logging() {
|
use log::LevelFilter as LF;
|
||||||
init_logging().ok();
|
let level = LF::from_str(&CONFIG.log_level()).expect("Valid log level");
|
||||||
}
|
init_logging(level).ok();
|
||||||
|
|
||||||
|
let extra_debug = match level {
|
||||||
|
LF::Trace | LF::Debug => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
check_db();
|
check_db();
|
||||||
check_rsa_keys();
|
check_rsa_keys();
|
||||||
check_web_vault();
|
check_web_vault();
|
||||||
migrations::run_migrations();
|
migrations::run_migrations();
|
||||||
|
|
||||||
launch_rocket();
|
create_icon_cache_folder();
|
||||||
|
|
||||||
|
launch_rocket(extra_debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn launch_info() {
|
fn launch_info() {
|
||||||
@@ -70,10 +79,23 @@ fn launch_info() {
|
|||||||
println!("\\--------------------------------------------------------------------/\n");
|
println!("\\--------------------------------------------------------------------/\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_logging() -> Result<(), fern::InitError> {
|
fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||||
use std::str::FromStr;
|
|
||||||
let mut logger = fern::Dispatch::new()
|
let mut logger = fern::Dispatch::new()
|
||||||
.format(|out, message, record| {
|
.level(level)
|
||||||
|
// Hide unknown certificate errors if using self-signed
|
||||||
|
.level_for("rustls::session", log::LevelFilter::Off)
|
||||||
|
// Hide failed to close stream messages
|
||||||
|
.level_for("hyper::server", log::LevelFilter::Warn)
|
||||||
|
// Silence rocket logs
|
||||||
|
.level_for("_", log::LevelFilter::Off)
|
||||||
|
.level_for("launch", log::LevelFilter::Off)
|
||||||
|
.level_for("launch_", log::LevelFilter::Off)
|
||||||
|
.level_for("rocket::rocket", log::LevelFilter::Off)
|
||||||
|
.level_for("rocket::fairing", log::LevelFilter::Off)
|
||||||
|
.chain(std::io::stdout());
|
||||||
|
|
||||||
|
if CONFIG.extended_logging() {
|
||||||
|
logger = logger.format(|out, message, record| {
|
||||||
out.finish(format_args!(
|
out.finish(format_args!(
|
||||||
"{}[{}][{}] {}",
|
"{}[{}][{}] {}",
|
||||||
chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"),
|
chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"),
|
||||||
@@ -81,13 +103,10 @@ fn init_logging() -> Result<(), fern::InitError> {
|
|||||||
record.level(),
|
record.level(),
|
||||||
message
|
message
|
||||||
))
|
))
|
||||||
})
|
});
|
||||||
.level(log::LevelFilter::from_str(&CONFIG.log_level()).expect("Valid log level"))
|
} else {
|
||||||
// Hide unknown certificate errors if using self-signed
|
logger = logger.format(|out, message, _| out.finish(format_args!("{}", message)));
|
||||||
.level_for("rustls::session", log::LevelFilter::Off)
|
}
|
||||||
// Hide failed to close stream messages
|
|
||||||
.level_for("hyper::server", log::LevelFilter::Warn)
|
|
||||||
.chain(std::io::stdout());
|
|
||||||
|
|
||||||
if let Some(log_file) = CONFIG.log_file() {
|
if let Some(log_file) = CONFIG.log_file() {
|
||||||
logger = logger.chain(fern::log_file(log_file)?);
|
logger = logger.chain(fern::log_file(log_file)?);
|
||||||
@@ -129,8 +148,7 @@ fn check_db() {
|
|||||||
let path = Path::new(&url);
|
let path = Path::new(&url);
|
||||||
|
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
use std::fs;
|
if create_dir_all(parent).is_err() {
|
||||||
if fs::create_dir_all(parent).is_err() {
|
|
||||||
error!("Error creating database directory");
|
error!("Error creating database directory");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -139,7 +157,7 @@ fn check_db() {
|
|||||||
// Turn on WAL in SQLite
|
// Turn on WAL in SQLite
|
||||||
if CONFIG.enable_db_wal() {
|
if CONFIG.enable_db_wal() {
|
||||||
use diesel::RunQueryDsl;
|
use diesel::RunQueryDsl;
|
||||||
let connection = db::get_connection().expect("Can't conect to DB");
|
let connection = db::get_connection().expect("Can't connect to DB");
|
||||||
diesel::sql_query("PRAGMA journal_mode=wal")
|
diesel::sql_query("PRAGMA journal_mode=wal")
|
||||||
.execute(&connection)
|
.execute(&connection)
|
||||||
.expect("Failed to turn on WAL");
|
.expect("Failed to turn on WAL");
|
||||||
@@ -148,6 +166,11 @@ fn check_db() {
|
|||||||
db::get_connection().expect("Can't connect to DB");
|
db::get_connection().expect("Can't connect to DB");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_icon_cache_folder() {
|
||||||
|
// Try to create the icon cache folder, and generate an error if it could not.
|
||||||
|
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache directory");
|
||||||
|
}
|
||||||
|
|
||||||
fn check_rsa_keys() {
|
fn check_rsa_keys() {
|
||||||
// If the RSA keys don't exist, try to create them
|
// If the RSA keys don't exist, try to create them
|
||||||
if !util::file_exists(&CONFIG.private_rsa_key()) || !util::file_exists(&CONFIG.public_rsa_key()) {
|
if !util::file_exists(&CONFIG.private_rsa_key()) || !util::file_exists(&CONFIG.public_rsa_key()) {
|
||||||
@@ -202,7 +225,9 @@ fn check_web_vault() {
|
|||||||
let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html");
|
let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html");
|
||||||
|
|
||||||
if !index_path.exists() {
|
if !index_path.exists() {
|
||||||
error!("Web vault is not found. To install it, please follow the steps in https://github.com/dani-garcia/bitwarden_rs/wiki/Building-binary#install-the-web-vault");
|
error!("Web vault is not found. To install it, please follow the steps in: ");
|
||||||
|
error!("https://github.com/dani-garcia/bitwarden_rs/wiki/Building-binary#install-the-web-vault");
|
||||||
|
error!("You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,33 +254,24 @@ mod migrations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn launch_rocket() {
|
fn launch_rocket(extra_debug: bool) {
|
||||||
// Create Rocket object, this stores current log level and sets it's own
|
// Create Rocket object, this stores current log level and sets it's own
|
||||||
let rocket = rocket::ignite();
|
let rocket = rocket::ignite();
|
||||||
|
|
||||||
// If we aren't logging the mounts, we force the logging level down
|
// If addding more base paths here, consider also adding them to
|
||||||
if !CONFIG.log_mounts() {
|
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
||||||
log::set_max_level(log::LevelFilter::Warn);
|
|
||||||
}
|
|
||||||
|
|
||||||
let rocket = rocket
|
let rocket = rocket
|
||||||
.mount("/", api::web_routes())
|
.mount("/", api::web_routes())
|
||||||
.mount("/api", api::core_routes())
|
.mount("/api", api::core_routes())
|
||||||
.mount("/admin", api::admin_routes())
|
.mount("/admin", api::admin_routes())
|
||||||
.mount("/identity", api::identity_routes())
|
.mount("/identity", api::identity_routes())
|
||||||
.mount("/icons", api::icons_routes())
|
.mount("/icons", api::icons_routes())
|
||||||
.mount("/notifications", api::notifications_routes());
|
.mount("/notifications", api::notifications_routes())
|
||||||
|
|
||||||
// Force the level up for the fairings, managed state and lauch
|
|
||||||
if !CONFIG.log_mounts() {
|
|
||||||
log::set_max_level(log::LevelFilter::max());
|
|
||||||
}
|
|
||||||
|
|
||||||
let rocket = rocket
|
|
||||||
.manage(db::init_pool())
|
.manage(db::init_pool())
|
||||||
.manage(api::start_notification_server())
|
.manage(api::start_notification_server())
|
||||||
.attach(util::AppHeaders())
|
.attach(util::AppHeaders())
|
||||||
.attach(util::CORS());
|
.attach(util::CORS())
|
||||||
|
.attach(util::BetterLogging(extra_debug));
|
||||||
|
|
||||||
// Launch and print error if there is one
|
// Launch and print error if there is one
|
||||||
// The launch will restore the original logging level
|
// The launch will restore the original logging level
|
||||||
|
@@ -39,7 +39,8 @@
|
|||||||
"Type": 1,
|
"Type": 1,
|
||||||
"Domains": [
|
"Domains": [
|
||||||
"apple.com",
|
"apple.com",
|
||||||
"icloud.com"
|
"icloud.com",
|
||||||
|
"tv.apple.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
@@ -106,7 +107,8 @@
|
|||||||
"windows.com",
|
"windows.com",
|
||||||
"microsoftonline.com",
|
"microsoftonline.com",
|
||||||
"office365.com",
|
"office365.com",
|
||||||
"microsoftstore.com"
|
"microsoftstore.com",
|
||||||
|
"xbox.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
@@ -760,7 +762,17 @@
|
|||||||
"superuser.com",
|
"superuser.com",
|
||||||
"stackoverflow.com",
|
"stackoverflow.com",
|
||||||
"serverfault.com",
|
"serverfault.com",
|
||||||
"mathoverflow.net"
|
"mathoverflow.net",
|
||||||
|
"askubuntu.com"
|
||||||
|
],
|
||||||
|
"Excluded": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Type": 75,
|
||||||
|
"Domains": [
|
||||||
|
"netcup.de",
|
||||||
|
"netcup.eu",
|
||||||
|
"customercontrolpanel.de"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
}
|
}
|
||||||
|
BIN
src/static/images/hibp.png
Normal file
BIN
src/static/images/hibp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.7 KiB |
@@ -33,7 +33,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top shadow">
|
<nav class="navbar navbar-expand-sm navbar-dark bg-dark fixed-top shadow">
|
||||||
<a class="navbar-brand" href="#">Bitwarden_rs</a>
|
<a class="navbar-brand" href="#">Bitwarden_rs</a>
|
||||||
<div class="navbar-collapse">
|
<div class="navbar-collapse">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
@@ -45,9 +45,20 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{#if version}}
|
|
||||||
<div class="navbar-text">Version: {{version}}</div>
|
<ul class="navbar-nav">
|
||||||
{{/if}}
|
{{#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="/admin/logout">Log Out</a>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{{> (page_content) }}
|
{{> (page_content) }}
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<form class="form-inline" method="post">
|
<form class="form-inline" method="post">
|
||||||
<input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token">
|
<input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token">
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary">Enter</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -191,7 +191,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function reload() { window.location.reload(); }
|
function reload() { window.location.reload(); }
|
||||||
function msg(text) { alert(text); reload(); }
|
function msg(text) { text && alert(text); reload(); }
|
||||||
function identicon(email) {
|
function identicon(email) {
|
||||||
const data = new Identicon(md5(email), { size: 48, format: 'svg' });
|
const data = new Identicon(md5(email), { size: 48, format: 'svg' });
|
||||||
return "data:image/svg+xml;base64," + data.toString();
|
return "data:image/svg+xml;base64," + data.toString();
|
||||||
|
6
src/static/templates/email/change_email.hbs
Normal file
6
src/static/templates/email/change_email.hbs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Your Email Change
|
||||||
|
<!---------------->
|
||||||
|
<html>
|
||||||
|
<p>To finalize changing your email address enter the following code in web vault: <b>{{token}}</b></p>
|
||||||
|
<p>If you did not try to change an email address, you can safely ignore this email.</p>
|
||||||
|
</html>
|
129
src/static/templates/email/change_email.html.hbs
Normal file
129
src/static/templates/email/change_email.html.hbs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
Your Email Change
|
||||||
|
<!---------------->
|
||||||
|
<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>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Bitwarden_rs</title>
|
||||||
|
</head>
|
||||||
|
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
body * {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.container-table {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 0 0 10px 0 !important;
|
||||||
|
}
|
||||||
|
.content-wrap {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
.invoice {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
border-right: none !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
padding-top: 10px !important;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
.indented {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<table class="body-wrap" cellpadding="0" cellspacing="0" 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; width: 100%;" bgcolor="#f6f6f6">
|
||||||
|
<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 valign="middle" class="aligncenter middle logo" 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; padding: 20px 0 10px;" align="center">
|
||||||
|
<img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||||
|
</td>
|
||||||
|
</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;">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
To finalize changing your email address enter the following code in web vault: <b 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;">{{token}}</b>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
If you did not try to change an email address, you can safely ignore this email.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||||
|
<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="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||||
|
<table cellpadding="0" cellspacing="0" 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 auto;">
|
||||||
|
<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 style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
src/static/templates/email/delete_account.hbs
Normal file
12
src/static/templates/email/delete_account.hbs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Delete Your Account
|
||||||
|
<!---------------->
|
||||||
|
<html>
|
||||||
|
<p>
|
||||||
|
click the link below to delete your account.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a href="{{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}}">
|
||||||
|
Delete Your Account</a>
|
||||||
|
</p>
|
||||||
|
<p>If you did not request this email to delete your account, you can safely ignore this email.</p>
|
||||||
|
</html>
|
137
src/static/templates/email/delete_account.html.hbs
Normal file
137
src/static/templates/email/delete_account.html.hbs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
Delete Your Account
|
||||||
|
<!---------------->
|
||||||
|
<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>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Bitwarden_rs</title>
|
||||||
|
</head>
|
||||||
|
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
body * {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.container-table {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 0 0 10px 0 !important;
|
||||||
|
}
|
||||||
|
.content-wrap {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
.invoice {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
border-right: none !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
padding-top: 10px !important;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
.indented {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<table class="body-wrap" cellpadding="0" cellspacing="0" 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; width: 100%;" bgcolor="#f6f6f6">
|
||||||
|
<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 valign="middle" class="aligncenter middle logo" 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; padding: 20px 0 10px;" align="center">
|
||||||
|
<img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||||
|
</td>
|
||||||
|
</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;">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
click the link below to delete your account.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
<a href="{{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}}"
|
||||||
|
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
Delete Your Account
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
If you did not request this email to delete your account, you can safely ignore this email.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||||
|
<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="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||||
|
<table cellpadding="0" cellspacing="0" 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 auto;">
|
||||||
|
<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 style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -1,3 +1,7 @@
|
|||||||
Sorry, you have no password hint...
|
Your master password hint
|
||||||
<!---------------->
|
<!---------------->
|
||||||
Sorry, you have not specified any password hint...
|
You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint.
|
||||||
|
|
||||||
|
If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.
|
||||||
|
|
||||||
|
If you did not request your master password hint you can safely ignore this email.
|
||||||
|
@@ -99,6 +99,11 @@ Sorry, you have no password hint...
|
|||||||
You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. <br 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;" />
|
You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. <br 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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
If you did not request your master password hint you can safely ignore this email.
|
If you did not request your master password hint you can safely ignore this email.
|
||||||
|
@@ -5,4 +5,6 @@ You (or someone) recently requested your master password hint.
|
|||||||
Your hint is: "{{hint}}"
|
Your hint is: "{{hint}}"
|
||||||
Log in: <a href="{{url}}">Web Vault</a>
|
Log in: <a href="{{url}}">Web Vault</a>
|
||||||
|
|
||||||
|
If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.
|
||||||
|
|
||||||
If you did not request your master password hint you can safely ignore this email.
|
If you did not request your master password hint you can safely ignore this email.
|
||||||
|
@@ -105,6 +105,11 @@ Your master password hint
|
|||||||
Log in: <a href="{{url}}">Web Vault</a>
|
Log in: <a href="{{url}}">Web Vault</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
If you did not request your master password hint you can safely ignore this email.
|
If you did not request your master password hint you can safely ignore this email.
|
||||||
|
12
src/static/templates/email/verify_email.hbs
Normal file
12
src/static/templates/email/verify_email.hbs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Verify Your Email
|
||||||
|
<!---------------->
|
||||||
|
<html>
|
||||||
|
<p>
|
||||||
|
Verify this email address for your account by clicking the link below.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}">
|
||||||
|
Verify Email Address Now</a>
|
||||||
|
</p>
|
||||||
|
<p>If you did not request to verify your account, you can safely ignore this email.</p>
|
||||||
|
</html>
|
137
src/static/templates/email/verify_email.html.hbs
Normal file
137
src/static/templates/email/verify_email.html.hbs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
Verify Your Email
|
||||||
|
<!---------------->
|
||||||
|
<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>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Bitwarden_rs</title>
|
||||||
|
</head>
|
||||||
|
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
body * {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.container-table {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 0 0 10px 0 !important;
|
||||||
|
}
|
||||||
|
.content-wrap {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
.invoice {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
border-right: none !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
padding-top: 10px !important;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
.indented {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<table class="body-wrap" cellpadding="0" cellspacing="0" 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; width: 100%;" bgcolor="#f6f6f6">
|
||||||
|
<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 valign="middle" class="aligncenter middle logo" 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; padding: 20px 0 10px;" align="center">
|
||||||
|
<img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||||
|
</td>
|
||||||
|
</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;">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
Verify this email address for your account by clicking the link below.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
<a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}"
|
||||||
|
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
Verify Email Address Now
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
If you did not request to verify your account, you can safely ignore this email.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||||
|
<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="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||||
|
<table cellpadding="0" cellspacing="0" 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 auto;">
|
||||||
|
<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 style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
src/static/templates/email/welcome.hbs
Normal file
8
src/static/templates/email/welcome.hbs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Welcome
|
||||||
|
<!---------------->
|
||||||
|
<html>
|
||||||
|
<p>
|
||||||
|
Thank you for creating an account at <a href="{{url}}">{{url}}</a>. You may now log in with your new account.
|
||||||
|
</p>
|
||||||
|
<p>If you did not request to create an account, you can safely ignore this email.</p>
|
||||||
|
</html>
|
129
src/static/templates/email/welcome.html.hbs
Normal file
129
src/static/templates/email/welcome.html.hbs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
Welcome
|
||||||
|
<!---------------->
|
||||||
|
<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>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Bitwarden_rs</title>
|
||||||
|
</head>
|
||||||
|
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
body * {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.container-table {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 0 0 10px 0 !important;
|
||||||
|
}
|
||||||
|
.content-wrap {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
.invoice {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
border-right: none !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
padding-top: 10px !important;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
.indented {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<table class="body-wrap" cellpadding="0" cellspacing="0" 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; width: 100%;" bgcolor="#f6f6f6">
|
||||||
|
<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 valign="middle" class="aligncenter middle logo" 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; padding: 20px 0 10px;" align="center">
|
||||||
|
<img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||||
|
</td>
|
||||||
|
</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;">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
Thank you for creating an account at <a href="{{url}}">{{url}}</a>. You may now log in with your new account.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
If you did not request to create an account, you can safely ignore this email.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||||
|
<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="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||||
|
<table cellpadding="0" cellspacing="0" 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 auto;">
|
||||||
|
<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 style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
src/static/templates/email/welcome_must_verify.hbs
Normal file
12
src/static/templates/email/welcome_must_verify.hbs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Welcome
|
||||||
|
<!---------------->
|
||||||
|
<html>
|
||||||
|
<p>
|
||||||
|
Thank you for creating an account at <a href="{{url}}">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}">
|
||||||
|
Verify Email Address Now</a>
|
||||||
|
</p>
|
||||||
|
<p>If you did not request to create an account, you can safely ignore this email.</p>
|
||||||
|
</html>
|
137
src/static/templates/email/welcome_must_verify.html.hbs
Normal file
137
src/static/templates/email/welcome_must_verify.html.hbs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
Welcome
|
||||||
|
<!---------------->
|
||||||
|
<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>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Bitwarden_rs</title>
|
||||||
|
</head>
|
||||||
|
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
body * {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.container-table {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 0 0 10px 0 !important;
|
||||||
|
}
|
||||||
|
.content-wrap {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
.invoice {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
border-right: none !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
padding-top: 10px !important;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
.indented {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<table class="body-wrap" cellpadding="0" cellspacing="0" 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; width: 100%;" bgcolor="#f6f6f6">
|
||||||
|
<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 valign="middle" class="aligncenter middle logo" 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; padding: 20px 0 10px;" align="center">
|
||||||
|
<img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||||
|
</td>
|
||||||
|
</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;">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<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;">
|
||||||
|
<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">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
Thank you for creating an account at <a href="{{url}}">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
<a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}"
|
||||||
|
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
Verify Email Address Now
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
If you did not request to create an account, you can safely ignore this email.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||||
|
<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="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||||
|
<table cellpadding="0" cellspacing="0" 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 auto;">
|
||||||
|
<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 style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
105
src/util.rs
105
src/util.rs
@@ -2,9 +2,9 @@
|
|||||||
// Web Headers and caching
|
// Web Headers and caching
|
||||||
//
|
//
|
||||||
use rocket::fairing::{Fairing, Info, Kind};
|
use rocket::fairing::{Fairing, Info, Kind};
|
||||||
|
use rocket::http::{ContentType, Header, HeaderMap, Method, Status};
|
||||||
use rocket::response::{self, Responder};
|
use rocket::response::{self, Responder};
|
||||||
use rocket::{Request, Response};
|
use rocket::{Data, Request, Response, Rocket};
|
||||||
use rocket::http::{Header, HeaderMap, ContentType, Method, Status};
|
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
pub struct AppHeaders();
|
pub struct AppHeaders();
|
||||||
@@ -55,7 +55,7 @@ impl Fairing for CORS {
|
|||||||
fn info(&self) -> Info {
|
fn info(&self) -> Info {
|
||||||
Info {
|
Info {
|
||||||
name: "CORS",
|
name: "CORS",
|
||||||
kind: Kind::Response
|
kind: Kind::Response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ impl Fairing for CORS {
|
|||||||
|
|
||||||
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));
|
||||||
@@ -107,6 +107,76 @@ impl<'r, R: Responder<'r>> Responder<'r> for Cached<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log all the routes from the main base paths list, and the attachments endoint
|
||||||
|
// Effectively ignores, any static file route, and the alive endpoint
|
||||||
|
const LOGGED_ROUTES: [&str; 6] = [
|
||||||
|
"/api",
|
||||||
|
"/admin",
|
||||||
|
"/identity",
|
||||||
|
"/icons",
|
||||||
|
"/notifications/hub/negotiate",
|
||||||
|
"/attachments",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Boolean is extra debug, when true, we ignore the whitelist above and also print the mounts
|
||||||
|
pub struct BetterLogging(pub bool);
|
||||||
|
impl Fairing for BetterLogging {
|
||||||
|
fn info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "Better Logging",
|
||||||
|
kind: Kind::Launch | Kind::Request | Kind::Response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_launch(&self, rocket: &Rocket) {
|
||||||
|
if self.0 {
|
||||||
|
info!(target: "routes", "Routes loaded:");
|
||||||
|
for route in rocket.routes() {
|
||||||
|
if route.rank < 0 {
|
||||||
|
info!(target: "routes", "{:<6} {}", route.method, route.uri);
|
||||||
|
} else {
|
||||||
|
info!(target: "routes", "{:<6} {} [{}]", route.method, route.uri, route.rank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = rocket.config();
|
||||||
|
let scheme = if config.tls_enabled() { "https" } else { "http" };
|
||||||
|
let addr = format!("{}://{}:{}", &scheme, &config.address, &config.port);
|
||||||
|
info!(target: "start", "Rocket has launched from {}", addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_request(&self, request: &mut Request<'_>, _data: &Data) {
|
||||||
|
let method = request.method();
|
||||||
|
if !self.0 && method == Method::Options {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let uri = request.uri();
|
||||||
|
let uri_path = uri.path();
|
||||||
|
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_path.starts_with(r)) {
|
||||||
|
match uri.query() {
|
||||||
|
Some(q) => info!(target: "request", "{} {}?{}", method, uri_path, &q[..q.len().min(30)]),
|
||||||
|
None => info!(target: "request", "{} {}", method, uri_path),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_response(&self, request: &Request, response: &mut Response) {
|
||||||
|
if !self.0 && request.method() == Method::Options {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let uri_path = request.uri().path();
|
||||||
|
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_path.starts_with(r)) {
|
||||||
|
let status = response.status();
|
||||||
|
if let Some(ref route) = request.route() {
|
||||||
|
info!(target: "response", "{} => {} {}", route, status.code, status.reason)
|
||||||
|
} else {
|
||||||
|
info!(target: "response", "{} {}", status.code, status.reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// File handling
|
// File handling
|
||||||
//
|
//
|
||||||
@@ -148,6 +218,33 @@ 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 {
|
||||||
|
Reference in New Issue
Block a user