From 2f9d7060bd966007bb3ac6435463a1389073d9b3 Mon Sep 17 00:00:00 2001
From: Stefan Melmuk <stefan.melmuk@gmail.com>
Date: Tue, 22 Nov 2022 04:40:20 +0100
Subject: [PATCH 1/4] check if sqlite folder exists

instead of creating the parent folders to a sqlite database
vaultwarden should just exit if it does not.

this should fix issues like #2835 when a wrongly configured
`DATABASE_URL` falls back to using sqlite
---
 src/config.rs | 10 +++++++++-
 src/db/mod.rs | 21 +++++++--------------
 2 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/src/config.rs b/src/config.rs
index 8427c37f..b8485af5 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -630,7 +630,15 @@ make_config! {
 
 fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
     // Validate connection URL is valid and DB feature is enabled
-    DbConnType::from_url(&cfg.database_url)?;
+    let url = &cfg.database_url;
+    if DbConnType::from_url(url)? == DbConnType::sqlite {
+        let path = std::path::Path::new(&url);
+        if let Some(parent) = path.parent() {
+            if !parent.exists() {
+                err!(format!("SQLite database directory `{}` does not exist", parent.display()));
+            }
+        }
+    }
 
     let limit = 256;
     if cfg.database_max_conns < 1 || cfg.database_max_conns > limit {
diff --git a/src/db/mod.rs b/src/db/mod.rs
index a84002cd..c2570d9d 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -424,22 +424,15 @@ mod sqlite_migrations {
     pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/sqlite");
 
     pub fn run_migrations() -> Result<(), super::Error> {
-        // Make sure the directory exists
-        let url = crate::CONFIG.database_url();
-        let path = std::path::Path::new(&url);
-
-        if let Some(parent) = path.parent() {
-            if std::fs::create_dir_all(parent).is_err() {
-                error!("Error creating database directory");
-                std::process::exit(1);
-            }
-        }
-
         use diesel::{Connection, RunQueryDsl};
-        // Make sure the database is up to date (create if it doesn't exist, or run the migrations)
-        let mut connection = diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url())?;
-        // Disable Foreign Key Checks during migration
+        let url = crate::CONFIG.database_url();
 
+        // Establish a connection to the sqlite database (this will create a new one, if it does
+        // not exist, and exit if there is an error).
+        let mut connection = diesel::sqlite::SqliteConnection::establish(&url)?;
+
+        // Run the migrations after successfully establishing a connection
+        // Disable Foreign Key Checks during migration
         // Scoped to a connection.
         diesel::sql_query("PRAGMA foreign_keys = OFF")
             .execute(&mut connection)

From 5a13efefd3e551e4fd47e2d748f6dab31ef1118d Mon Sep 17 00:00:00 2001
From: Stefan Melmuk <stefan.melmuk@gmail.com>
Date: Tue, 22 Nov 2022 05:46:51 +0100
Subject: [PATCH 2/4] only check sqlite parent if there could be one

---
 src/config.rs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/config.rs b/src/config.rs
index b8485af5..4aa8d649 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -631,11 +631,11 @@ make_config! {
 fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
     // Validate connection URL is valid and DB feature is enabled
     let url = &cfg.database_url;
-    if DbConnType::from_url(url)? == DbConnType::sqlite {
+    if DbConnType::from_url(url)? == DbConnType::sqlite && url.contains('/') {
         let path = std::path::Path::new(&url);
         if let Some(parent) = path.parent() {
-            if !parent.exists() {
-                err!(format!("SQLite database directory `{}` does not exist", parent.display()));
+            if !parent.is_dir() {
+                err!(format!("SQLite database directory `{}` does not exist or is not a directory", parent.display()));
             }
         }
     }

From b186813049a5c1c92aa44d118d7339f9ceaf9bb8 Mon Sep 17 00:00:00 2001
From: BlackDex <black.dex@gmail.com>
Date: Sun, 20 Nov 2022 19:15:45 +0100
Subject: [PATCH 3/4] Add Organizational event logging feature

This PR adds event/audit logging support for organizations.
By default this feature is disabled, since it does log a lot and adds
extra database transactions.

All events are touched except a few, since we do not support those
features (yet), like SSO for example.

This feature is tested with multiple clients and all database types.

Fixes #229
---
 .env.template                                 |  27 +-
 .../2022-10-18-170602_add_events/down.sql     |   1 +
 .../mysql/2022-10-18-170602_add_events/up.sql |  19 +
 .../2022-10-18-170602_add_events/down.sql     |   1 +
 .../2022-10-18-170602_add_events/up.sql       |  19 +
 .../2022-10-18-170602_add_events/down.sql     |   1 +
 .../2022-10-18-170602_add_events/up.sql       |  19 +
 src/api/admin.rs                              |  44 +-
 src/api/core/accounts.rs                      |  25 +-
 src/api/core/ciphers.rs                       | 334 ++++++++++---
 src/api/core/events.rs                        | 341 +++++++++++++
 src/api/core/mod.rs                           |  10 +
 src/api/core/organizations.rs                 | 467 +++++++++++++++---
 src/api/core/two_factor/authenticator.rs      |  21 +-
 src/api/core/two_factor/duo.rs                |  31 +-
 src/api/core/two_factor/email.rs              |  37 +-
 src/api/core/two_factor/mod.rs                |  25 +-
 src/api/core/two_factor/webauthn.rs           |  39 +-
 src/api/core/two_factor/yubikey.rs            |  27 +-
 src/api/identity.rs                           | 147 ++++--
 src/api/mod.rs                                |   1 +
 src/config.rs                                 |  19 +-
 src/db/models/event.rs                        | 318 ++++++++++++
 src/db/models/mod.rs                          |   2 +
 src/db/models/organization.rs                 |  28 +-
 src/db/schemas/mysql/schema.rs                |  23 +
 src/db/schemas/postgresql/schema.rs           |  23 +
 src/db/schemas/sqlite/schema.rs               |  24 +
 src/error.rs                                  |  34 +-
 src/main.rs                                   |  11 +
 src/util.rs                                   |   9 +-
 31 files changed, 1887 insertions(+), 240 deletions(-)
 create mode 100644 migrations/mysql/2022-10-18-170602_add_events/down.sql
 create mode 100644 migrations/mysql/2022-10-18-170602_add_events/up.sql
 create mode 100644 migrations/postgresql/2022-10-18-170602_add_events/down.sql
 create mode 100644 migrations/postgresql/2022-10-18-170602_add_events/up.sql
 create mode 100644 migrations/sqlite/2022-10-18-170602_add_events/down.sql
 create mode 100644 migrations/sqlite/2022-10-18-170602_add_events/up.sql
 create mode 100644 src/api/core/events.rs
 create mode 100644 src/db/models/event.rs

diff --git a/.env.template b/.env.template
index e06c09bd..736b6463 100644
--- a/.env.template
+++ b/.env.template
@@ -1,13 +1,14 @@
+# shellcheck disable=SC2034,SC2148
 ## Vaultwarden Configuration File
 ## Uncomment any of the following lines to change the defaults
 ##
 ## Be aware that most of these settings will be overridden if they were changed
 ## in the admin interface. Those overrides are stored within DATA_FOLDER/config.json .
 ##
-## By default, vaultwarden expects for this file to be named ".env" and located
+## By default, Vaultwarden expects for this file to be named ".env" and located
 ## in the current working directory. If this is not the case, the environment
 ## variable ENV_FILE can be set to the location of this file prior to starting
-## vaultwarden.
+## Vaultwarden.
 
 ## Main data folder
 # DATA_FOLDER=data
@@ -80,11 +81,27 @@
 ## This setting applies globally to all users.
 # EMERGENCY_ACCESS_ALLOWED=true
 
+## Controls whether event logging is enabled for organizations
+## This setting applies to organizations.
+## Default this is disabled. Also check the EVENT_CLEANUP_SCHEDULE and EVENTS_DAYS_RETAIN settings.
+# ORG_EVENTS_ENABLED=false
+
+## Number of days to retain events stored in the database.
+## If unset (the default), events are kept indefently and also disables the scheduled job!
+# EVENTS_DAYS_RETAIN=
+
 ## Job scheduler settings
 ##
 ## Job schedules use a cron-like syntax (as parsed by https://crates.io/crates/cron),
 ## and are always in terms of UTC time (regardless of your local time zone settings).
 ##
+## The schedule format is a bit different from crontab as crontab does not contains seconds.
+## You can test the the format here: https://crontab.guru, but remove the first digit!
+## SEC  MIN   HOUR   DAY OF MONTH    MONTH   DAY OF WEEK
+## "0   30   9,12,15     1,15       May-Aug  Mon,Wed,Fri"
+## "0   30     *          *            *          *     "
+## "0   30     1          *            *          *     "
+##
 ## How often (in ms) the job scheduler thread checks for jobs that need running.
 ## Set to 0 to globally disable scheduled jobs.
 # JOB_POLL_INTERVAL_MS=30000
@@ -108,6 +125,10 @@
 ## Cron schedule of the job that grants emergency access requests that have met the required wait time.
 ## Defaults to hourly (5 minutes after the hour). Set blank to disable this job.
 # EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 5 * * * *"
+##
+## Cron schedule of the job that cleans old events from the event table.
+## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start.
+# EVENT_CLEANUP_SCHEDULE="0 10 0 * * *"
 
 ## Enable extended logging, which shows timestamps and targets in the logs
 # EXTENDED_LOGGING=true
@@ -133,7 +154,7 @@
 ## Enable WAL for the DB
 ## Set to false to avoid enabling WAL during startup.
 ## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
-## this setting only prevents vaultwarden from automatically enabling it on start.
+## this setting only prevents Vaultwarden from automatically enabling it on start.
 ## Please read project wiki page about this setting first before changing the value as it can
 ## cause performance degradation or might render the service unable to start.
 # ENABLE_DB_WAL=true
diff --git a/migrations/mysql/2022-10-18-170602_add_events/down.sql b/migrations/mysql/2022-10-18-170602_add_events/down.sql
new file mode 100644
index 00000000..8b975bc3
--- /dev/null
+++ b/migrations/mysql/2022-10-18-170602_add_events/down.sql
@@ -0,0 +1 @@
+DROP TABLE event;
diff --git a/migrations/mysql/2022-10-18-170602_add_events/up.sql b/migrations/mysql/2022-10-18-170602_add_events/up.sql
new file mode 100644
index 00000000..24e1c8cd
--- /dev/null
+++ b/migrations/mysql/2022-10-18-170602_add_events/up.sql
@@ -0,0 +1,19 @@
+CREATE TABLE event (
+  uuid               CHAR(36)    NOT NULL PRIMARY KEY,
+  event_type         INTEGER     NOT NULL,
+  user_uuid          CHAR(36),
+  org_uuid           CHAR(36),
+  cipher_uuid        CHAR(36),
+  collection_uuid    CHAR(36),
+  group_uuid         CHAR(36),
+  org_user_uuid      CHAR(36),
+  act_user_uuid      CHAR(36),
+  device_type        INTEGER,
+  ip_address         TEXT,
+  event_date         DATETIME    NOT NULL,
+  policy_uuid        CHAR(36),
+  provider_uuid      CHAR(36),
+  provider_user_uuid CHAR(36),
+  provider_org_uuid  CHAR(36),
+  UNIQUE (uuid)
+);
diff --git a/migrations/postgresql/2022-10-18-170602_add_events/down.sql b/migrations/postgresql/2022-10-18-170602_add_events/down.sql
new file mode 100644
index 00000000..8b975bc3
--- /dev/null
+++ b/migrations/postgresql/2022-10-18-170602_add_events/down.sql
@@ -0,0 +1 @@
+DROP TABLE event;
diff --git a/migrations/postgresql/2022-10-18-170602_add_events/up.sql b/migrations/postgresql/2022-10-18-170602_add_events/up.sql
new file mode 100644
index 00000000..2d107b41
--- /dev/null
+++ b/migrations/postgresql/2022-10-18-170602_add_events/up.sql
@@ -0,0 +1,19 @@
+CREATE TABLE event (
+  uuid               CHAR(36)        NOT NULL PRIMARY KEY,
+  event_type         INTEGER     NOT NULL,
+  user_uuid          CHAR(36),
+  org_uuid           CHAR(36),
+  cipher_uuid        CHAR(36),
+  collection_uuid    CHAR(36),
+  group_uuid         CHAR(36),
+  org_user_uuid      CHAR(36),
+  act_user_uuid      CHAR(36),
+  device_type        INTEGER,
+  ip_address         TEXT,
+  event_date         TIMESTAMP    NOT NULL,
+  policy_uuid        CHAR(36),
+  provider_uuid      CHAR(36),
+  provider_user_uuid CHAR(36),
+  provider_org_uuid  CHAR(36),
+  UNIQUE (uuid)
+);
diff --git a/migrations/sqlite/2022-10-18-170602_add_events/down.sql b/migrations/sqlite/2022-10-18-170602_add_events/down.sql
new file mode 100644
index 00000000..8b975bc3
--- /dev/null
+++ b/migrations/sqlite/2022-10-18-170602_add_events/down.sql
@@ -0,0 +1 @@
+DROP TABLE event;
diff --git a/migrations/sqlite/2022-10-18-170602_add_events/up.sql b/migrations/sqlite/2022-10-18-170602_add_events/up.sql
new file mode 100644
index 00000000..e6b32722
--- /dev/null
+++ b/migrations/sqlite/2022-10-18-170602_add_events/up.sql
@@ -0,0 +1,19 @@
+CREATE TABLE event (
+  uuid               TEXT        NOT NULL PRIMARY KEY,
+  event_type         INTEGER     NOT NULL,
+  user_uuid          TEXT,
+  org_uuid           TEXT,
+  cipher_uuid        TEXT,
+  collection_uuid    TEXT,
+  group_uuid         TEXT,
+  org_user_uuid      TEXT,
+  act_user_uuid      TEXT,
+  device_type        INTEGER,
+  ip_address         TEXT,
+  event_date         DATETIME    NOT NULL,
+  policy_uuid        TEXT,
+  provider_uuid      TEXT,
+  provider_user_uuid TEXT,
+  provider_org_uuid  TEXT,
+  UNIQUE (uuid)
+);
diff --git a/src/api/admin.rs b/src/api/admin.rs
index 30ffdec2..6e10f0b6 100644
--- a/src/api/admin.rs
+++ b/src/api/admin.rs
@@ -13,7 +13,7 @@ use rocket::{
 };
 
 use crate::{
-    api::{ApiResult, EmptyResult, JsonResult, NumberOrString},
+    api::{core::log_event, ApiResult, EmptyResult, JsonResult, NumberOrString},
     auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
     config::ConfigBuilder,
     db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
@@ -81,6 +81,8 @@ const DT_FMT: &str = "%Y-%m-%d %H:%M:%S %Z";
 
 const BASE_TEMPLATE: &str = "admin/base";
 
+const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000";
+
 fn admin_path() -> String {
     format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
 }
@@ -361,9 +363,27 @@ async fn get_user_json(uuid: String, _token: AdminToken, mut conn: DbConn) -> Js
 }
 
 #[post("/users/<uuid>/delete")]
-async fn delete_user(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
+async fn delete_user(uuid: String, _token: AdminToken, mut conn: DbConn, ip: ClientIp) -> EmptyResult {
     let user = get_user_or_404(&uuid, &mut conn).await?;
-    user.delete(&mut conn).await
+
+    // Get the user_org records before deleting the actual user
+    let user_orgs = UserOrganization::find_any_state_by_user(&uuid, &mut conn).await;
+    let res = user.delete(&mut conn).await;
+
+    for user_org in user_orgs {
+        log_event(
+            EventType::OrganizationUserRemoved as i32,
+            &user_org.uuid,
+            user_org.org_uuid,
+            String::from(ACTING_ADMIN_USER),
+            14, // Use UnknownBrowser type
+            &ip.ip,
+            &mut conn,
+        )
+        .await;
+    }
+
+    res
 }
 
 #[post("/users/<uuid>/deauth")]
@@ -409,7 +429,12 @@ struct UserOrgTypeData {
 }
 
 #[post("/users/org_type", data = "<data>")]
-async fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
+async fn update_user_org_type(
+    data: Json<UserOrgTypeData>,
+    _token: AdminToken,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> EmptyResult {
     let data: UserOrgTypeData = data.into_inner();
 
     let mut user_to_edit =
@@ -444,6 +469,17 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, m
         }
     }
 
+    log_event(
+        EventType::OrganizationUserUpdated as i32,
+        &user_to_edit.uuid,
+        data.org_uuid,
+        String::from(ACTING_ADMIN_USER),
+        14, // Use UnknownBrowser type
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     user_to_edit.atype = new_type;
     user_to_edit.save(&mut conn).await
 }
diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs
index 2cd500d1..3315fbce 100644
--- a/src/api/core/accounts.rs
+++ b/src/api/core/accounts.rs
@@ -3,8 +3,10 @@ use rocket::serde::json::Json;
 use serde_json::Value;
 
 use crate::{
-    api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType},
-    auth::{decode_delete, decode_invite, decode_verify_email, Headers},
+    api::{
+        core::log_user_event, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
+    },
+    auth::{decode_delete, decode_invite, decode_verify_email, ClientIp, Headers},
     crypto,
     db::{models::*, DbConn},
     mail, CONFIG,
@@ -268,7 +270,12 @@ struct ChangePassData {
 }
 
 #[post("/accounts/password", data = "<data>")]
-async fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
+async fn post_password(
+    data: JsonUpcase<ChangePassData>,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> EmptyResult {
     let data: ChangePassData = data.into_inner().data;
     let mut user = headers.user;
 
@@ -279,6 +286,8 @@ async fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, mut c
     user.password_hint = clean_password_hint(&data.MasterPasswordHint);
     enforce_password_hint_setting(&user.password_hint)?;
 
+    log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
+
     user.set_password(
         &data.NewMasterPasswordHash,
         Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]),
@@ -334,7 +343,13 @@ struct KeyData {
 }
 
 #[post("/accounts/key", data = "<data>")]
-async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
+async fn post_rotatekey(
+    data: JsonUpcase<KeyData>,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> EmptyResult {
     let data: KeyData = data.into_inner().data;
 
     if !headers.user.check_valid_password(&data.MasterPasswordHash) {
@@ -373,7 +388,7 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: D
 
         // Prevent triggering cipher updates via WebSockets by settings UpdateType::None
         // The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
-        update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None)
+        update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None)
             .await?
     }
 
diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs
index 39635efb..c8c741d4 100644
--- a/src/api/core/ciphers.rs
+++ b/src/api/core/ciphers.rs
@@ -10,8 +10,8 @@ use rocket::{
 use serde_json::Value;
 
 use crate::{
-    api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType},
-    auth::Headers,
+    api::{self, core::log_event, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType},
+    auth::{ClientIp, Headers},
     crypto,
     db::{models::*, DbConn, DbPool},
     CONFIG,
@@ -247,9 +247,10 @@ async fn post_ciphers_admin(
     data: JsonUpcase<ShareCipherData>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
-    post_ciphers_create(data, headers, conn, nt).await
+    post_ciphers_create(data, headers, conn, ip, nt).await
 }
 
 /// Called when creating a new org-owned cipher, or cloning a cipher (whether
@@ -260,6 +261,7 @@ async fn post_ciphers_create(
     data: JsonUpcase<ShareCipherData>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
     let mut data: ShareCipherData = data.into_inner().data;
@@ -287,12 +289,18 @@ async fn post_ciphers_create(
     // or otherwise), we can just ignore this field entirely.
     data.Cipher.LastKnownRevisionDate = None;
 
-    share_cipher_by_uuid(&cipher.uuid, data, &headers, &mut conn, &nt).await
+    share_cipher_by_uuid(&cipher.uuid, data, &headers, &mut conn, &ip, &nt).await
 }
 
 /// Called when creating a new user-owned cipher.
 #[post("/ciphers", data = "<data>")]
-async fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
+async fn post_ciphers(
+    data: JsonUpcase<CipherData>,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> JsonResult {
     let mut data: CipherData = data.into_inner().data;
 
     // The web/browser clients set this field to null as expected, but the
@@ -302,7 +310,7 @@ async fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, mut conn:
     data.LastKnownRevisionDate = None;
 
     let mut cipher = Cipher::new(data.Type, data.Name.clone());
-    update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::CipherCreate).await?;
+    update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::CipherCreate).await?;
 
     Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
 }
@@ -329,12 +337,14 @@ async fn enforce_personal_ownership_policy(
     Ok(())
 }
 
+#[allow(clippy::too_many_arguments)]
 pub async fn update_cipher_from_data(
     cipher: &mut Cipher,
     data: CipherData,
     headers: &Headers,
     shared_to_collection: bool,
     conn: &mut DbConn,
+    ip: &ClientIp,
     nt: &Notify<'_>,
     ut: UpdateType,
 ) -> EmptyResult {
@@ -356,6 +366,9 @@ pub async fn update_cipher_from_data(
         err!("Organization mismatch. Please resync the client before updating the cipher")
     }
 
+    // Check if this cipher is being transferred from a personal to an organization vault
+    let transfer_cipher = cipher.organization_uuid.is_none() && data.OrganizationId.is_some();
+
     if let Some(org_id) = data.OrganizationId {
         match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await {
             None => err!("You don't have permission to add item to organization"),
@@ -460,6 +473,26 @@ pub async fn update_cipher_from_data(
     cipher.set_favorite(data.Favorite, &headers.user.uuid, conn).await?;
 
     if ut != UpdateType::None {
+        // Only log events for organizational ciphers
+        if let Some(org_uuid) = &cipher.organization_uuid {
+            let event_type = match (&ut, transfer_cipher) {
+                (UpdateType::CipherCreate, true) => EventType::CipherCreated,
+                (UpdateType::CipherUpdate, true) => EventType::CipherShared,
+                (_, _) => EventType::CipherUpdated,
+            };
+
+            log_event(
+                event_type as i32,
+                &cipher.uuid,
+                String::from(org_uuid),
+                headers.user.uuid.clone(),
+                headers.device.atype,
+                &ip.ip,
+                conn,
+            )
+            .await;
+        }
+
         nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await).await;
     }
 
@@ -488,6 +521,7 @@ async fn post_ciphers_import(
     data: JsonUpcase<ImportData>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
     enforce_personal_ownership_policy(None, &headers, &mut conn).await?;
@@ -516,7 +550,8 @@ async fn post_ciphers_import(
         cipher_data.FolderId = folder_uuid;
 
         let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
-        update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None).await?;
+        update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None)
+            .await?;
     }
 
     let mut user = headers.user;
@@ -532,9 +567,10 @@ async fn put_cipher_admin(
     data: JsonUpcase<CipherData>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
-    put_cipher(uuid, data, headers, conn, nt).await
+    put_cipher(uuid, data, headers, conn, ip, nt).await
 }
 
 #[post("/ciphers/<uuid>/admin", data = "<data>")]
@@ -543,9 +579,10 @@ async fn post_cipher_admin(
     data: JsonUpcase<CipherData>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
-    post_cipher(uuid, data, headers, conn, nt).await
+    post_cipher(uuid, data, headers, conn, ip, nt).await
 }
 
 #[post("/ciphers/<uuid>", data = "<data>")]
@@ -554,9 +591,10 @@ async fn post_cipher(
     data: JsonUpcase<CipherData>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
-    put_cipher(uuid, data, headers, conn, nt).await
+    put_cipher(uuid, data, headers, conn, ip, nt).await
 }
 
 #[put("/ciphers/<uuid>", data = "<data>")]
@@ -565,6 +603,7 @@ async fn put_cipher(
     data: JsonUpcase<CipherData>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
     let data: CipherData = data.into_inner().data;
@@ -583,7 +622,7 @@ async fn put_cipher(
         err!("Cipher is not write accessible")
     }
 
-    update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::CipherUpdate).await?;
+    update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::CipherUpdate).await?;
 
     Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
 }
@@ -600,8 +639,9 @@ async fn put_collections_update(
     data: JsonUpcase<CollectionsAdminData>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    post_collections_admin(uuid, data, headers, conn).await
+    post_collections_admin(uuid, data, headers, conn, ip).await
 }
 
 #[post("/ciphers/<uuid>/collections", data = "<data>")]
@@ -610,8 +650,9 @@ async fn post_collections_update(
     data: JsonUpcase<CollectionsAdminData>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    post_collections_admin(uuid, data, headers, conn).await
+    post_collections_admin(uuid, data, headers, conn, ip).await
 }
 
 #[put("/ciphers/<uuid>/collections-admin", data = "<data>")]
@@ -620,8 +661,9 @@ async fn put_collections_admin(
     data: JsonUpcase<CollectionsAdminData>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    post_collections_admin(uuid, data, headers, conn).await
+    post_collections_admin(uuid, data, headers, conn, ip).await
 }
 
 #[post("/ciphers/<uuid>/collections-admin", data = "<data>")]
@@ -630,6 +672,7 @@ async fn post_collections_admin(
     data: JsonUpcase<CollectionsAdminData>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
     let data: CollectionsAdminData = data.into_inner().data;
 
@@ -665,6 +708,17 @@ async fn post_collections_admin(
         }
     }
 
+    log_event(
+        EventType::CipherUpdatedCollections as i32,
+        &cipher.uuid,
+        cipher.organization_uuid.unwrap(),
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     Ok(())
 }
 
@@ -681,11 +735,12 @@ async fn post_cipher_share(
     data: JsonUpcase<ShareCipherData>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
     let data: ShareCipherData = data.into_inner().data;
 
-    share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await
+    share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &ip, &nt).await
 }
 
 #[put("/ciphers/<uuid>/share", data = "<data>")]
@@ -694,11 +749,12 @@ async fn put_cipher_share(
     data: JsonUpcase<ShareCipherData>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
     let data: ShareCipherData = data.into_inner().data;
 
-    share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await
+    share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &ip, &nt).await
 }
 
 #[derive(Deserialize)]
@@ -713,6 +769,7 @@ async fn put_cipher_share_selected(
     data: JsonUpcase<ShareSelectedCipherData>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
     let mut data: ShareSelectedCipherData = data.into_inner().data;
@@ -740,7 +797,7 @@ async fn put_cipher_share_selected(
         };
 
         match shared_cipher_data.Cipher.Id.take() {
-            Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &mut conn, &nt).await?,
+            Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &mut conn, &ip, &nt).await?,
             None => err!("Request missing ids field"),
         };
     }
@@ -753,6 +810,7 @@ async fn share_cipher_by_uuid(
     data: ShareCipherData,
     headers: &Headers,
     conn: &mut DbConn,
+    ip: &ClientIp,
     nt: &Notify<'_>,
 ) -> JsonResult {
     let mut cipher = match Cipher::find_by_uuid(uuid, conn).await {
@@ -768,37 +826,30 @@ async fn share_cipher_by_uuid(
 
     let mut shared_to_collection = false;
 
-    match data.Cipher.OrganizationId.clone() {
-        // If we don't get an organization ID, we don't do anything
-        // No error because this is used when using the Clone functionality
-        None => {}
-        Some(organization_uuid) => {
-            for uuid in &data.CollectionIds {
-                match Collection::find_by_uuid_and_org(uuid, &organization_uuid, conn).await {
-                    None => err!("Invalid collection ID provided"),
-                    Some(collection) => {
-                        if collection.is_writable_by_user(&headers.user.uuid, conn).await {
-                            CollectionCipher::save(&cipher.uuid, &collection.uuid, conn).await?;
-                            shared_to_collection = true;
-                        } else {
-                            err!("No rights to modify the collection")
-                        }
+    if let Some(organization_uuid) = &data.Cipher.OrganizationId {
+        for uuid in &data.CollectionIds {
+            match Collection::find_by_uuid_and_org(uuid, organization_uuid, conn).await {
+                None => err!("Invalid collection ID provided"),
+                Some(collection) => {
+                    if collection.is_writable_by_user(&headers.user.uuid, conn).await {
+                        CollectionCipher::save(&cipher.uuid, &collection.uuid, conn).await?;
+                        shared_to_collection = true;
+                    } else {
+                        err!("No rights to modify the collection")
                     }
                 }
             }
         }
     };
 
-    update_cipher_from_data(
-        &mut cipher,
-        data.Cipher,
-        headers,
-        shared_to_collection,
-        conn,
-        nt,
-        UpdateType::CipherUpdate,
-    )
-    .await?;
+    // When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate.
+    let ut = if data.Cipher.LastKnownRevisionDate.is_some() {
+        UpdateType::CipherUpdate
+    } else {
+        UpdateType::CipherCreate
+    };
+
+    update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, ip, nt, ut).await?;
 
     Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await))
 }
@@ -893,6 +944,7 @@ async fn save_attachment(
     data: Form<UploadData<'_>>,
     headers: &Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> Result<(Cipher, DbConn), crate::error::Error> {
     let cipher = match Cipher::find_by_uuid(&cipher_uuid, &mut conn).await {
@@ -1011,6 +1063,19 @@ async fn save_attachment(
 
     nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&mut conn).await).await;
 
+    if let Some(org_uuid) = &cipher.organization_uuid {
+        log_event(
+            EventType::CipherAttachmentCreated as i32,
+            &cipher.uuid,
+            String::from(org_uuid),
+            headers.user.uuid.clone(),
+            headers.device.atype,
+            &ip.ip,
+            &mut conn,
+        )
+        .await;
+    }
+
     Ok((cipher, conn))
 }
 
@@ -1025,6 +1090,7 @@ async fn post_attachment_v2_data(
     data: Form<UploadData<'_>>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
     let attachment = match Attachment::find_by_id(&attachment_id, &mut conn).await {
@@ -1033,7 +1099,7 @@ async fn post_attachment_v2_data(
         None => err!("Attachment doesn't exist"),
     };
 
-    save_attachment(attachment, uuid, data, &headers, conn, nt).await?;
+    save_attachment(attachment, uuid, data, &headers, conn, ip, nt).await?;
 
     Ok(())
 }
@@ -1045,13 +1111,14 @@ async fn post_attachment(
     data: Form<UploadData<'_>>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
     // Setting this as None signifies to save_attachment() that it should create
     // the attachment database record as well as saving the data to disk.
     let attachment = None;
 
-    let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, nt).await?;
+    let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, ip, nt).await?;
 
     Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
 }
@@ -1062,9 +1129,10 @@ async fn post_attachment_admin(
     data: Form<UploadData<'_>>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
-    post_attachment(uuid, data, headers, conn, nt).await
+    post_attachment(uuid, data, headers, conn, ip, nt).await
 }
 
 #[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
@@ -1074,10 +1142,11 @@ async fn post_attachment_share(
     data: Form<UploadData<'_>>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
-    _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await?;
-    post_attachment(uuid, data, headers, conn, nt).await
+    _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &ip, &nt).await?;
+    post_attachment(uuid, data, headers, conn, ip, nt).await
 }
 
 #[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
@@ -1086,9 +1155,10 @@ async fn delete_attachment_post_admin(
     attachment_id: String,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    delete_attachment(uuid, attachment_id, headers, conn, nt).await
+    delete_attachment(uuid, attachment_id, headers, conn, ip, nt).await
 }
 
 #[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
@@ -1097,9 +1167,10 @@ async fn delete_attachment_post(
     attachment_id: String,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    delete_attachment(uuid, attachment_id, headers, conn, nt).await
+    delete_attachment(uuid, attachment_id, headers, conn, ip, nt).await
 }
 
 #[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
@@ -1108,9 +1179,10 @@ async fn delete_attachment(
     attachment_id: String,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await
+    _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &ip, &nt).await
 }
 
 #[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
@@ -1119,39 +1191,70 @@ async fn delete_attachment_admin(
     attachment_id: String,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await
+    _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &ip, &nt).await
 }
 
 #[post("/ciphers/<uuid>/delete")]
-async fn delete_cipher_post(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
-    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await
+async fn delete_cipher_post(
+    uuid: String,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> EmptyResult {
+    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &ip, &nt).await // permanent delete
 }
 
 #[post("/ciphers/<uuid>/delete-admin")]
-async fn delete_cipher_post_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
-    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await
+async fn delete_cipher_post_admin(
+    uuid: String,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> EmptyResult {
+    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &ip, &nt).await // permanent delete
 }
 
 #[put("/ciphers/<uuid>/delete")]
-async fn delete_cipher_put(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
-    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await
+async fn delete_cipher_put(
+    uuid: String,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> EmptyResult {
+    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &ip, &nt).await // soft delete
 }
 
 #[put("/ciphers/<uuid>/delete-admin")]
-async fn delete_cipher_put_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
-    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await
+async fn delete_cipher_put_admin(
+    uuid: String,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> EmptyResult {
+    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &ip, &nt).await
 }
 
 #[delete("/ciphers/<uuid>")]
-async fn delete_cipher(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
-    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await
+async fn delete_cipher(uuid: String, headers: Headers, mut conn: DbConn, ip: ClientIp, nt: Notify<'_>) -> EmptyResult {
+    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &ip, &nt).await // permanent delete
 }
 
 #[delete("/ciphers/<uuid>/admin")]
-async fn delete_cipher_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
-    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await
+async fn delete_cipher_admin(
+    uuid: String,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> EmptyResult {
+    _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &ip, &nt).await // permanent delete
 }
 
 #[delete("/ciphers", data = "<data>")]
@@ -1159,9 +1262,10 @@ async fn delete_cipher_selected(
     data: JsonUpcase<Value>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    _delete_multiple_ciphers(data, headers, conn, false, nt).await
+    _delete_multiple_ciphers(data, headers, conn, false, ip, nt).await // permanent delete
 }
 
 #[post("/ciphers/delete", data = "<data>")]
@@ -1169,9 +1273,10 @@ async fn delete_cipher_selected_post(
     data: JsonUpcase<Value>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    _delete_multiple_ciphers(data, headers, conn, false, nt).await
+    _delete_multiple_ciphers(data, headers, conn, false, ip, nt).await // permanent delete
 }
 
 #[put("/ciphers/delete", data = "<data>")]
@@ -1179,9 +1284,10 @@ async fn delete_cipher_selected_put(
     data: JsonUpcase<Value>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    _delete_multiple_ciphers(data, headers, conn, true, nt).await // soft delete
+    _delete_multiple_ciphers(data, headers, conn, true, ip, nt).await // soft delete
 }
 
 #[delete("/ciphers/admin", data = "<data>")]
@@ -1189,9 +1295,10 @@ async fn delete_cipher_selected_admin(
     data: JsonUpcase<Value>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    delete_cipher_selected(data, headers, conn, nt).await
+    _delete_multiple_ciphers(data, headers, conn, false, ip, nt).await // permanent delete
 }
 
 #[post("/ciphers/delete-admin", data = "<data>")]
@@ -1199,9 +1306,10 @@ async fn delete_cipher_selected_post_admin(
     data: JsonUpcase<Value>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    delete_cipher_selected_post(data, headers, conn, nt).await
+    _delete_multiple_ciphers(data, headers, conn, false, ip, nt).await // permanent delete
 }
 
 #[put("/ciphers/delete-admin", data = "<data>")]
@@ -1209,19 +1317,32 @@ async fn delete_cipher_selected_put_admin(
     data: JsonUpcase<Value>,
     headers: Headers,
     conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
-    delete_cipher_selected_put(data, headers, conn, nt).await
+    _delete_multiple_ciphers(data, headers, conn, true, ip, nt).await // soft delete
 }
 
 #[put("/ciphers/<uuid>/restore")]
-async fn restore_cipher_put(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
-    _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await
+async fn restore_cipher_put(
+    uuid: String,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> JsonResult {
+    _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &ip, &nt).await
 }
 
 #[put("/ciphers/<uuid>/restore-admin")]
-async fn restore_cipher_put_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
-    _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await
+async fn restore_cipher_put_admin(
+    uuid: String,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> JsonResult {
+    _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &ip, &nt).await
 }
 
 #[put("/ciphers/restore", data = "<data>")]
@@ -1229,9 +1350,10 @@ async fn restore_cipher_selected(
     data: JsonUpcase<Value>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> JsonResult {
-    _restore_multiple_ciphers(data, &headers, &mut conn, &nt).await
+    _restore_multiple_ciphers(data, &headers, &mut conn, ip, &nt).await
 }
 
 #[derive(Deserialize)]
@@ -1303,6 +1425,7 @@ async fn delete_all(
     data: JsonUpcase<PasswordData>,
     headers: Headers,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
     let data: PasswordData = data.into_inner().data;
@@ -1323,6 +1446,18 @@ async fn delete_all(
                     if user_org.atype == UserOrgType::Owner {
                         Cipher::delete_all_by_organization(&org_data.org_id, &mut conn).await?;
                         nt.send_user_update(UpdateType::Vault, &user).await;
+
+                        log_event(
+                            EventType::OrganizationPurgedVault as i32,
+                            &org_data.org_id,
+                            org_data.org_id.clone(),
+                            user.uuid,
+                            headers.device.atype,
+                            &ip.ip,
+                            &mut conn,
+                        )
+                        .await;
+
                         Ok(())
                     } else {
                         err!("You don't have permission to purge the organization vault");
@@ -1354,6 +1489,7 @@ async fn _delete_cipher_by_uuid(
     headers: &Headers,
     conn: &mut DbConn,
     soft_delete: bool,
+    ip: &ClientIp,
     nt: &Notify<'_>,
 ) -> EmptyResult {
     let mut cipher = match Cipher::find_by_uuid(uuid, conn).await {
@@ -1374,6 +1510,16 @@ async fn _delete_cipher_by_uuid(
         nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(conn).await).await;
     }
 
+    if let Some(org_uuid) = cipher.organization_uuid {
+        let event_type = match soft_delete {
+            true => EventType::CipherSoftDeleted as i32,
+            false => EventType::CipherDeleted as i32,
+        };
+
+        log_event(event_type, &cipher.uuid, org_uuid, headers.user.uuid.clone(), headers.device.atype, &ip.ip, conn)
+            .await;
+    }
+
     Ok(())
 }
 
@@ -1382,6 +1528,7 @@ async fn _delete_multiple_ciphers(
     headers: Headers,
     mut conn: DbConn,
     soft_delete: bool,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
     let data: Value = data.into_inner().data;
@@ -1395,7 +1542,7 @@ async fn _delete_multiple_ciphers(
     };
 
     for uuid in uuids {
-        if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &mut conn, soft_delete, &nt).await {
+        if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &mut conn, soft_delete, &ip, &nt).await {
             return error;
         };
     }
@@ -1403,7 +1550,13 @@ async fn _delete_multiple_ciphers(
     Ok(())
 }
 
-async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>) -> JsonResult {
+async fn _restore_cipher_by_uuid(
+    uuid: &str,
+    headers: &Headers,
+    conn: &mut DbConn,
+    ip: &ClientIp,
+    nt: &Notify<'_>,
+) -> JsonResult {
     let mut cipher = match Cipher::find_by_uuid(uuid, conn).await {
         Some(cipher) => cipher,
         None => err!("Cipher doesn't exist"),
@@ -1417,6 +1570,19 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon
     cipher.save(conn).await?;
 
     nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(conn).await).await;
+    if let Some(org_uuid) = &cipher.organization_uuid {
+        log_event(
+            EventType::CipherRestored as i32,
+            &cipher.uuid.clone(),
+            String::from(org_uuid),
+            headers.user.uuid.clone(),
+            headers.device.atype,
+            &ip.ip,
+            conn,
+        )
+        .await;
+    }
+
     Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await))
 }
 
@@ -1424,6 +1590,7 @@ async fn _restore_multiple_ciphers(
     data: JsonUpcase<Value>,
     headers: &Headers,
     conn: &mut DbConn,
+    ip: ClientIp,
     nt: &Notify<'_>,
 ) -> JsonResult {
     let data: Value = data.into_inner().data;
@@ -1438,7 +1605,7 @@ async fn _restore_multiple_ciphers(
 
     let mut ciphers: Vec<Value> = Vec::new();
     for uuid in uuids {
-        match _restore_cipher_by_uuid(uuid, headers, conn, nt).await {
+        match _restore_cipher_by_uuid(uuid, headers, conn, &ip, nt).await {
             Ok(json) => ciphers.push(json.into_inner()),
             err => return err,
         }
@@ -1456,6 +1623,7 @@ async fn _delete_cipher_attachment_by_id(
     attachment_id: &str,
     headers: &Headers,
     conn: &mut DbConn,
+    ip: &ClientIp,
     nt: &Notify<'_>,
 ) -> EmptyResult {
     let attachment = match Attachment::find_by_id(attachment_id, conn).await {
@@ -1479,6 +1647,18 @@ async fn _delete_cipher_attachment_by_id(
     // Delete attachment
     attachment.delete(conn).await?;
     nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(conn).await).await;
+    if let Some(org_uuid) = cipher.organization_uuid {
+        log_event(
+            EventType::CipherAttachmentDeleted as i32,
+            &cipher.uuid,
+            org_uuid,
+            headers.user.uuid.clone(),
+            headers.device.atype,
+            &ip.ip,
+            conn,
+        )
+        .await;
+    }
     Ok(())
 }
 
diff --git a/src/api/core/events.rs b/src/api/core/events.rs
new file mode 100644
index 00000000..43102712
--- /dev/null
+++ b/src/api/core/events.rs
@@ -0,0 +1,341 @@
+use std::net::IpAddr;
+
+use chrono::NaiveDateTime;
+use rocket::{form::FromForm, serde::json::Json, Route};
+use serde_json::Value;
+
+use crate::{
+    api::{EmptyResult, JsonResult, JsonUpcaseVec},
+    auth::{AdminHeaders, ClientIp, Headers},
+    db::{
+        models::{Cipher, Event, UserOrganization},
+        DbConn, DbPool,
+    },
+    util::parse_date,
+    CONFIG,
+};
+
+/// ###############################################################################################################
+/// /api routes
+pub fn routes() -> Vec<Route> {
+    routes![get_org_events, get_cipher_events, get_user_events,]
+}
+
+#[derive(FromForm)]
+#[allow(non_snake_case)]
+struct EventRange {
+    start: String,
+    end: String,
+    #[field(name = "continuationToken")]
+    continuation_token: Option<String>,
+}
+
+// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41
+#[get("/organizations/<org_id>/events?<data..>")]
+async fn get_org_events(org_id: String, data: EventRange, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
+    // Return an empty vec when we org events are disabled.
+    // This prevents client errors
+    let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
+        Vec::with_capacity(0)
+    } else {
+        let start_date = parse_date(&data.start);
+        let end_date = if let Some(before_date) = &data.continuation_token {
+            parse_date(before_date)
+        } else {
+            parse_date(&data.end)
+        };
+
+        Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &mut conn)
+            .await
+            .iter()
+            .map(|e| e.to_json())
+            .collect()
+    };
+
+    Ok(Json(json!({
+        "Data": events_json,
+        "Object": "list",
+        "ContinuationToken": get_continuation_token(&events_json),
+    })))
+}
+
+#[get("/ciphers/<cipher_id>/events?<data..>")]
+async fn get_cipher_events(cipher_id: String, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult {
+    // Return an empty vec when we org events are disabled.
+    // This prevents client errors
+    let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
+        Vec::with_capacity(0)
+    } else {
+        let mut events_json = Vec::with_capacity(0);
+        if UserOrganization::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &mut conn).await {
+            let start_date = parse_date(&data.start);
+            let end_date = if let Some(before_date) = &data.continuation_token {
+                parse_date(before_date)
+            } else {
+                parse_date(&data.end)
+            };
+
+            events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &mut conn)
+                .await
+                .iter()
+                .map(|e| e.to_json())
+                .collect()
+        }
+        events_json
+    };
+
+    Ok(Json(json!({
+        "Data": events_json,
+        "Object": "list",
+        "ContinuationToken": get_continuation_token(&events_json),
+    })))
+}
+
+#[get("/organizations/<org_id>/users/<user_org_id>/events?<data..>")]
+async fn get_user_events(
+    org_id: String,
+    user_org_id: String,
+    data: EventRange,
+    _headers: AdminHeaders,
+    mut conn: DbConn,
+) -> JsonResult {
+    // Return an empty vec when we org events are disabled.
+    // This prevents client errors
+    let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
+        Vec::with_capacity(0)
+    } else {
+        let start_date = parse_date(&data.start);
+        let end_date = if let Some(before_date) = &data.continuation_token {
+            parse_date(before_date)
+        } else {
+            parse_date(&data.end)
+        };
+
+        Event::find_by_org_and_user_org(&org_id, &user_org_id, &start_date, &end_date, &mut conn)
+            .await
+            .iter()
+            .map(|e| e.to_json())
+            .collect()
+    };
+
+    Ok(Json(json!({
+        "Data": events_json,
+        "Object": "list",
+        "ContinuationToken": get_continuation_token(&events_json),
+    })))
+}
+
+fn get_continuation_token(events_json: &Vec<Value>) -> Option<&str> {
+    // When the length of the vec equals the max page_size there probably is more data
+    // When it is less, then all events are loaded.
+    if events_json.len() as i64 == Event::PAGE_SIZE {
+        if let Some(last_event) = events_json.last() {
+            last_event["date"].as_str()
+        } else {
+            None
+        }
+    } else {
+        None
+    }
+}
+
+/// ###############################################################################################################
+/// /events routes
+pub fn main_routes() -> Vec<Route> {
+    routes![post_events_collect,]
+}
+
+#[derive(Deserialize, Debug)]
+#[allow(non_snake_case)]
+struct EventCollection {
+    // Mandatory
+    Type: i32,
+    Date: String,
+
+    // Optional
+    CipherId: Option<String>,
+    OrganizationId: Option<String>,
+}
+
+// Upstream:
+// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Events/Controllers/CollectController.cs
+// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
+#[post("/collect", format = "application/json", data = "<data>")]
+async fn post_events_collect(
+    data: JsonUpcaseVec<EventCollection>,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> EmptyResult {
+    if !CONFIG.org_events_enabled() {
+        return Ok(());
+    }
+
+    for event in data.iter().map(|d| &d.data) {
+        let event_date = parse_date(&event.Date);
+        match event.Type {
+            1000..=1099 => {
+                _log_user_event(
+                    event.Type,
+                    &headers.user.uuid,
+                    headers.device.atype,
+                    Some(event_date),
+                    &ip.ip,
+                    &mut conn,
+                )
+                .await;
+            }
+            1600..=1699 => {
+                if let Some(org_uuid) = &event.OrganizationId {
+                    _log_event(
+                        event.Type,
+                        org_uuid,
+                        String::from(org_uuid),
+                        &headers.user.uuid,
+                        headers.device.atype,
+                        Some(event_date),
+                        &ip.ip,
+                        &mut conn,
+                    )
+                    .await;
+                }
+            }
+            _ => {
+                if let Some(cipher_uuid) = &event.CipherId {
+                    if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await {
+                        if let Some(org_uuid) = cipher.organization_uuid {
+                            _log_event(
+                                event.Type,
+                                cipher_uuid,
+                                org_uuid,
+                                &headers.user.uuid,
+                                headers.device.atype,
+                                Some(event_date),
+                                &ip.ip,
+                                &mut conn,
+                            )
+                            .await;
+                        }
+                    }
+                }
+            }
+        }
+    }
+    Ok(())
+}
+
+pub async fn log_user_event(event_type: i32, user_uuid: &str, device_type: i32, ip: &IpAddr, conn: &mut DbConn) {
+    if !CONFIG.org_events_enabled() {
+        return;
+    }
+    _log_user_event(event_type, user_uuid, device_type, None, ip, conn).await;
+}
+
+async fn _log_user_event(
+    event_type: i32,
+    user_uuid: &str,
+    device_type: i32,
+    event_date: Option<NaiveDateTime>,
+    ip: &IpAddr,
+    conn: &mut DbConn,
+) {
+    let orgs = UserOrganization::get_org_uuid_by_user(user_uuid, conn).await;
+    let mut events: Vec<Event> = Vec::with_capacity(orgs.len() + 1); // We need an event per org and one without an org
+
+    // Upstream saves the event also without any org_uuid.
+    let mut event = Event::new(event_type, event_date);
+    event.user_uuid = Some(String::from(user_uuid));
+    event.act_user_uuid = Some(String::from(user_uuid));
+    event.device_type = Some(device_type);
+    event.ip_address = Some(ip.to_string());
+    events.push(event);
+
+    // For each org a user is a member of store these events per org
+    for org_uuid in orgs {
+        let mut event = Event::new(event_type, event_date);
+        event.user_uuid = Some(String::from(user_uuid));
+        event.org_uuid = Some(org_uuid);
+        event.act_user_uuid = Some(String::from(user_uuid));
+        event.device_type = Some(device_type);
+        event.ip_address = Some(ip.to_string());
+        events.push(event);
+    }
+
+    Event::save_user_event(events, conn).await.unwrap_or(());
+}
+
+pub async fn log_event(
+    event_type: i32,
+    source_uuid: &str,
+    org_uuid: String,
+    act_user_uuid: String,
+    device_type: i32,
+    ip: &IpAddr,
+    conn: &mut DbConn,
+) {
+    if !CONFIG.org_events_enabled() {
+        return;
+    }
+    _log_event(event_type, source_uuid, org_uuid, &act_user_uuid, device_type, None, ip, conn).await;
+}
+
+#[allow(clippy::too_many_arguments)]
+async fn _log_event(
+    event_type: i32,
+    source_uuid: &str,
+    org_uuid: String,
+    act_user_uuid: &str,
+    device_type: i32,
+    event_date: Option<NaiveDateTime>,
+    ip: &IpAddr,
+    conn: &mut DbConn,
+) {
+    // Create a new empty event
+    let mut event = Event::new(event_type, event_date);
+    match event_type {
+        // 1000..=1099 Are user events, they need to be logged via log_user_event()
+        // Collection Events
+        1100..=1199 => {
+            event.cipher_uuid = Some(String::from(source_uuid));
+        }
+        // Collection Events
+        1300..=1399 => {
+            event.collection_uuid = Some(String::from(source_uuid));
+        }
+        // Group Events
+        1400..=1499 => {
+            event.group_uuid = Some(String::from(source_uuid));
+        }
+        // Org User Events
+        1500..=1599 => {
+            event.org_user_uuid = Some(String::from(source_uuid));
+        }
+        // 1600..=1699 Are organizational events, and they do not need the source_uuid
+        // Policy Events
+        1700..=1799 => {
+            event.policy_uuid = Some(String::from(source_uuid));
+        }
+        // Ignore others
+        _ => {}
+    }
+
+    event.org_uuid = Some(org_uuid);
+    event.act_user_uuid = Some(String::from(act_user_uuid));
+    event.device_type = Some(device_type);
+    event.ip_address = Some(ip.to_string());
+    event.save(conn).await.unwrap_or(());
+}
+
+pub async fn event_cleanup_job(pool: DbPool) {
+    debug!("Start events cleanup job");
+    if CONFIG.events_days_retain().is_none() {
+        debug!("events_days_retain is not configured, abort");
+        return;
+    }
+
+    if let Ok(mut conn) = pool.get().await {
+        Event::clean_events(&mut conn).await.ok();
+    } else {
+        error!("Failed to get DB connection while trying to cleanup the events table")
+    }
+}
diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs
index 0df9a9dc..885fae81 100644
--- a/src/api/core/mod.rs
+++ b/src/api/core/mod.rs
@@ -1,6 +1,7 @@
 pub mod accounts;
 mod ciphers;
 mod emergency_access;
+mod events;
 mod folders;
 mod organizations;
 mod sends;
@@ -9,6 +10,7 @@ pub mod two_factor;
 pub use ciphers::purge_trashed_ciphers;
 pub use ciphers::{CipherSyncData, CipherSyncType};
 pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
+pub use events::{event_cleanup_job, log_event, log_user_event};
 pub use sends::purge_sends;
 pub use two_factor::send_incomplete_2fa_notifications;
 
@@ -22,6 +24,7 @@ pub fn routes() -> Vec<Route> {
     routes.append(&mut accounts::routes());
     routes.append(&mut ciphers::routes());
     routes.append(&mut emergency_access::routes());
+    routes.append(&mut events::routes());
     routes.append(&mut folders::routes());
     routes.append(&mut organizations::routes());
     routes.append(&mut two_factor::routes());
@@ -34,6 +37,13 @@ pub fn routes() -> Vec<Route> {
     routes
 }
 
+pub fn events_routes() -> Vec<Route> {
+    let mut routes = Vec::new();
+    routes.append(&mut events::main_routes());
+
+    routes
+}
+
 //
 // Move this somewhere else
 //
diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs
index 744b0a12..57a982f9 100644
--- a/src/api/core/organizations.rs
+++ b/src/api/core/organizations.rs
@@ -5,11 +5,11 @@ use serde_json::Value;
 
 use crate::{
     api::{
-        core::{CipherSyncData, CipherSyncType},
+        core::{log_event, CipherSyncData, CipherSyncType},
         ApiResult, EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordData,
         UpdateType,
     },
-    auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
+    auth::{decode_invite, AdminHeaders, ClientIp, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
     db::{models::*, DbConn},
     error::Error,
     mail,
@@ -203,7 +203,7 @@ async fn post_delete_organization(
 }
 
 #[post("/organizations/<org_id>/leave")]
-async fn leave_organization(org_id: String, headers: Headers, mut conn: DbConn) -> EmptyResult {
+async fn leave_organization(org_id: String, headers: Headers, mut conn: DbConn, ip: ClientIp) -> EmptyResult {
     match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await {
         None => err!("User not part of organization"),
         Some(user_org) => {
@@ -213,6 +213,17 @@ async fn leave_organization(org_id: String, headers: Headers, mut conn: DbConn)
                 err!("The last owner can't leave")
             }
 
+            log_event(
+                EventType::OrganizationUserRemoved as i32,
+                &user_org.uuid,
+                org_id,
+                headers.user.uuid.clone(),
+                headers.device.atype,
+                &ip.ip,
+                &mut conn,
+            )
+            .await;
+
             user_org.delete(&mut conn).await
         }
     }
@@ -232,16 +243,18 @@ async fn put_organization(
     headers: OwnerHeaders,
     data: JsonUpcase<OrganizationUpdateData>,
     conn: DbConn,
+    ip: ClientIp,
 ) -> JsonResult {
-    post_organization(org_id, headers, data, conn).await
+    post_organization(org_id, headers, data, conn, ip).await
 }
 
 #[post("/organizations/<org_id>", data = "<data>")]
 async fn post_organization(
     org_id: String,
-    _headers: OwnerHeaders,
+    headers: OwnerHeaders,
     data: JsonUpcase<OrganizationUpdateData>,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> JsonResult {
     let data: OrganizationUpdateData = data.into_inner().data;
 
@@ -254,6 +267,18 @@ async fn post_organization(
     org.billing_email = data.BillingEmail;
 
     org.save(&mut conn).await?;
+
+    log_event(
+        EventType::OrganizationUpdated as i32,
+        &org_id,
+        org_id.clone(),
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     Ok(Json(org.to_json()))
 }
 
@@ -290,6 +315,7 @@ async fn post_organization_collections(
     headers: ManagerHeadersLoose,
     data: JsonUpcase<NewCollectionData>,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> JsonResult {
     let data: NewCollectionData = data.into_inner().data;
 
@@ -307,6 +333,17 @@ async fn post_organization_collections(
     let collection = Collection::new(org.uuid, data.Name);
     collection.save(&mut conn).await?;
 
+    log_event(
+        EventType::CollectionCreated as i32,
+        &collection.uuid,
+        org_id,
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     for group in data.Groups {
         CollectionGroup::new(collection.uuid.clone(), group.Id, group.ReadOnly, group.HidePasswords)
             .save(&mut conn)
@@ -330,17 +367,19 @@ async fn put_organization_collection_update(
     headers: ManagerHeaders,
     data: JsonUpcase<NewCollectionData>,
     conn: DbConn,
+    ip: ClientIp,
 ) -> JsonResult {
-    post_organization_collection_update(org_id, col_id, headers, data, conn).await
+    post_organization_collection_update(org_id, col_id, headers, data, conn, ip).await
 }
 
 #[post("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
 async fn post_organization_collection_update(
     org_id: String,
     col_id: String,
-    _headers: ManagerHeaders,
+    headers: ManagerHeaders,
     data: JsonUpcase<NewCollectionData>,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> JsonResult {
     let data: NewCollectionData = data.into_inner().data;
 
@@ -361,6 +400,17 @@ async fn post_organization_collection_update(
     collection.name = data.Name;
     collection.save(&mut conn).await?;
 
+    log_event(
+        EventType::CollectionUpdated as i32,
+        &collection.uuid,
+        org_id,
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?;
 
     for group in data.Groups {
@@ -415,13 +465,24 @@ async fn post_organization_collection_delete_user(
 async fn delete_organization_collection(
     org_id: String,
     col_id: String,
-    _headers: ManagerHeaders,
+    headers: ManagerHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
     match Collection::find_by_uuid(&col_id, &mut conn).await {
         None => err!("Collection not found"),
         Some(collection) => {
             if collection.org_uuid == org_id {
+                log_event(
+                    EventType::CollectionDeleted as i32,
+                    &collection.uuid,
+                    org_id,
+                    headers.user.uuid.clone(),
+                    headers.device.atype,
+                    &ip.ip,
+                    &mut conn,
+                )
+                .await;
                 collection.delete(&mut conn).await
             } else {
                 err!("Collection and Organization id do not match")
@@ -444,8 +505,9 @@ async fn post_organization_collection_delete(
     headers: ManagerHeaders,
     _data: JsonUpcase<DeleteCollectionData>,
     conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    delete_organization_collection(org_id, col_id, headers, conn).await
+    delete_organization_collection(org_id, col_id, headers, conn, ip).await
 }
 
 #[get("/organizations/<org_id>/collections/<coll_id>/details")]
@@ -632,6 +694,7 @@ async fn send_invite(
     data: JsonUpcase<InviteData>,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
     let data: InviteData = data.into_inner().data;
 
@@ -700,6 +763,17 @@ async fn send_invite(
 
         new_user.save(&mut conn).await?;
 
+        log_event(
+            EventType::OrganizationUserInvited as i32,
+            &new_user.uuid,
+            org_id.clone(),
+            headers.user.uuid.clone(),
+            headers.device.atype,
+            &ip.ip,
+            &mut conn,
+        )
+        .await;
+
         if CONFIG.mail_enabled() {
             let org_name = match Organization::find_by_uuid(&org_id, &mut conn).await {
                 Some(org) => org.name,
@@ -882,6 +956,7 @@ async fn bulk_confirm_invite(
     data: JsonUpcase<Value>,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> Json<Value> {
     let data = data.into_inner().data;
 
@@ -891,7 +966,7 @@ async fn bulk_confirm_invite(
             for invite in keys {
                 let org_user_id = invite["Id"].as_str().unwrap_or_default();
                 let user_key = invite["Key"].as_str().unwrap_or_default();
-                let err_msg = match _confirm_invite(&org_id, org_user_id, user_key, &headers, &mut conn).await {
+                let err_msg = match _confirm_invite(&org_id, org_user_id, user_key, &headers, &mut conn, &ip).await {
                     Ok(_) => String::new(),
                     Err(e) => format!("{:?}", e),
                 };
@@ -922,10 +997,11 @@ async fn confirm_invite(
     data: JsonUpcase<Value>,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
     let data = data.into_inner().data;
     let user_key = data["Key"].as_str().unwrap_or_default();
-    _confirm_invite(&org_id, &org_user_id, user_key, &headers, &mut conn).await
+    _confirm_invite(&org_id, &org_user_id, user_key, &headers, &mut conn, &ip).await
 }
 
 async fn _confirm_invite(
@@ -934,6 +1010,7 @@ async fn _confirm_invite(
     key: &str,
     headers: &AdminHeaders,
     conn: &mut DbConn,
+    ip: &ClientIp,
 ) -> EmptyResult {
     if key.is_empty() || org_user_id.is_empty() {
         err!("Key or UserId is not set, unable to process request");
@@ -969,6 +1046,17 @@ async fn _confirm_invite(
     user_to_confirm.status = UserOrgStatus::Confirmed as i32;
     user_to_confirm.akey = key.to_string();
 
+    log_event(
+        EventType::OrganizationUserConfirmed as i32,
+        &user_to_confirm.uuid,
+        String::from(org_id),
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        conn,
+    )
+    .await;
+
     if CONFIG.mail_enabled() {
         let org_name = match Organization::find_by_uuid(org_id, conn).await {
             Some(org) => org.name,
@@ -1009,8 +1097,9 @@ async fn put_organization_user(
     data: JsonUpcase<EditUserData>,
     headers: AdminHeaders,
     conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    edit_user(org_id, org_user_id, data, headers, conn).await
+    edit_user(org_id, org_user_id, data, headers, conn, ip).await
 }
 
 #[post("/organizations/<org_id>/users/<org_user_id>", data = "<data>", rank = 1)]
@@ -1020,6 +1109,7 @@ async fn edit_user(
     data: JsonUpcase<EditUserData>,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
     let data: EditUserData = data.into_inner().data;
 
@@ -1095,6 +1185,17 @@ async fn edit_user(
         }
     }
 
+    log_event(
+        EventType::OrganizationUserUpdated as i32,
+        &user_to_edit.uuid,
+        org_id.clone(),
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     user_to_edit.save(&mut conn).await
 }
 
@@ -1104,12 +1205,13 @@ async fn bulk_delete_user(
     data: JsonUpcase<OrgBulkIds>,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> Json<Value> {
     let data: OrgBulkIds = data.into_inner().data;
 
     let mut bulk_response = Vec::new();
     for org_user_id in data.Ids {
-        let err_msg = match _delete_user(&org_id, &org_user_id, &headers, &mut conn).await {
+        let err_msg = match _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await {
             Ok(_) => String::new(),
             Err(e) => format!("{:?}", e),
         };
@@ -1131,11 +1233,34 @@ async fn bulk_delete_user(
 }
 
 #[delete("/organizations/<org_id>/users/<org_user_id>")]
-async fn delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult {
-    _delete_user(&org_id, &org_user_id, &headers, &mut conn).await
+async fn delete_user(
+    org_id: String,
+    org_user_id: String,
+    headers: AdminHeaders,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> EmptyResult {
+    _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await
 }
 
-async fn _delete_user(org_id: &str, org_user_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult {
+#[post("/organizations/<org_id>/users/<org_user_id>/delete")]
+async fn post_delete_user(
+    org_id: String,
+    org_user_id: String,
+    headers: AdminHeaders,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> EmptyResult {
+    _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await
+}
+
+async fn _delete_user(
+    org_id: &str,
+    org_user_id: &str,
+    headers: &AdminHeaders,
+    conn: &mut DbConn,
+    ip: &ClientIp,
+) -> EmptyResult {
     let user_to_delete = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await {
         Some(user) => user,
         None => err!("User to delete isn't member of the organization"),
@@ -1152,12 +1277,18 @@ async fn _delete_user(org_id: &str, org_user_id: &str, headers: &AdminHeaders, c
         }
     }
 
-    user_to_delete.delete(conn).await
-}
+    log_event(
+        EventType::OrganizationUserRemoved as i32,
+        &user_to_delete.uuid,
+        String::from(org_id),
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        conn,
+    )
+    .await;
 
-#[post("/organizations/<org_id>/users/<org_user_id>/delete")]
-async fn post_delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
-    delete_user(org_id, org_user_id, headers, conn).await
+    user_to_delete.delete(conn).await
 }
 
 #[post("/organizations/<org_id>/users/public-keys", data = "<data>")]
@@ -1223,6 +1354,7 @@ async fn post_org_import(
     data: JsonUpcase<ImportData>,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
     nt: Notify<'_>,
 ) -> EmptyResult {
     let data: ImportData = data.into_inner().data;
@@ -1249,7 +1381,9 @@ async fn post_org_import(
     let mut ciphers = Vec::new();
     for cipher_data in data.Ciphers {
         let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
-        update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None).await.ok();
+        update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None)
+            .await
+            .ok();
         ciphers.push(cipher);
     }
 
@@ -1333,8 +1467,9 @@ async fn put_policy(
     org_id: String,
     pol_type: i32,
     data: Json<PolicyData>,
-    _headers: AdminHeaders,
+    headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> JsonResult {
     let data: PolicyData = data.into_inner();
 
@@ -1360,6 +1495,18 @@ async fn put_policy(
 
                     mail::send_2fa_removed_from_org(&user.email, &org.name).await?;
                 }
+
+                log_event(
+                    EventType::OrganizationUserRemoved as i32,
+                    &member.uuid,
+                    org_id.clone(),
+                    headers.user.uuid.clone(),
+                    headers.device.atype,
+                    &ip.ip,
+                    &mut conn,
+                )
+                .await;
+
                 member.delete(&mut conn).await?;
             }
         }
@@ -1382,6 +1529,18 @@ async fn put_policy(
 
                     mail::send_single_org_removed_from_org(&user.email, &org.name).await?;
                 }
+
+                log_event(
+                    EventType::OrganizationUserRemoved as i32,
+                    &member.uuid,
+                    org_id.clone(),
+                    headers.user.uuid.clone(),
+                    headers.device.atype,
+                    &ip.ip,
+                    &mut conn,
+                )
+                .await;
+
                 member.delete(&mut conn).await?;
             }
         }
@@ -1389,13 +1548,24 @@ async fn put_policy(
 
     let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &mut conn).await {
         Some(p) => p,
-        None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()),
+        None => OrgPolicy::new(org_id.clone(), pol_type_enum, "{}".to_string()),
     };
 
     policy.enabled = data.enabled;
     policy.data = serde_json::to_string(&data.data)?;
     policy.save(&mut conn).await?;
 
+    log_event(
+        EventType::PolicyUpdated as i32,
+        &policy.uuid,
+        org_id,
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     Ok(Json(policy.to_json()))
 }
 
@@ -1467,7 +1637,13 @@ struct OrgImportData {
 }
 
 #[post("/organizations/<org_id>/import", data = "<data>")]
-async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
+async fn import(
+    org_id: String,
+    data: JsonUpcase<OrgImportData>,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> EmptyResult {
     let data = data.into_inner().data;
 
     // TODO: Currently we aren't storing the externalId's anywhere, so we also don't have a way
@@ -1487,6 +1663,17 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header
             // If user is marked for deletion and it exists, delete it
             if let Some(user_org) = UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await
             {
+                log_event(
+                    EventType::OrganizationUserRemoved as i32,
+                    &user_org.uuid,
+                    org_id.clone(),
+                    headers.user.uuid.clone(),
+                    headers.device.atype,
+                    &ip.ip,
+                    &mut conn,
+                )
+                .await;
+
                 user_org.delete(&mut conn).await?;
             }
 
@@ -1506,6 +1693,17 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header
 
                 new_org_user.save(&mut conn).await?;
 
+                log_event(
+                    EventType::OrganizationUserInvited as i32,
+                    &new_org_user.uuid,
+                    org_id.clone(),
+                    headers.user.uuid.clone(),
+                    headers.device.atype,
+                    &ip.ip,
+                    &mut conn,
+                )
+                .await;
+
                 if CONFIG.mail_enabled() {
                     let org_name = match Organization::find_by_uuid(&org_id, &mut conn).await {
                         Some(org) => org.name,
@@ -1531,6 +1729,17 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header
         for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User, &mut conn).await {
             if let Some(user_email) = User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.email) {
                 if !data.Users.iter().any(|u| u.Email == user_email) {
+                    log_event(
+                        EventType::OrganizationUserRemoved as i32,
+                        &user_org.uuid,
+                        org_id.clone(),
+                        headers.user.uuid.clone(),
+                        headers.device.atype,
+                        &ip.ip,
+                        &mut conn,
+                    )
+                    .await;
+
                     user_org.delete(&mut conn).await?;
                 }
             }
@@ -1547,8 +1756,9 @@ async fn deactivate_organization_user(
     org_user_id: String,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn).await
+    _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await
 }
 
 // Pre web-vault v2022.9.x endpoint
@@ -1558,8 +1768,9 @@ async fn bulk_deactivate_organization_user(
     data: JsonUpcase<Value>,
     headers: AdminHeaders,
     conn: DbConn,
+    ip: ClientIp,
 ) -> Json<Value> {
-    bulk_revoke_organization_user(org_id, data, headers, conn).await
+    bulk_revoke_organization_user(org_id, data, headers, conn, ip).await
 }
 
 #[put("/organizations/<org_id>/users/<org_user_id>/revoke")]
@@ -1568,8 +1779,9 @@ async fn revoke_organization_user(
     org_user_id: String,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn).await
+    _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await
 }
 
 #[put("/organizations/<org_id>/users/revoke", data = "<data>")]
@@ -1578,6 +1790,7 @@ async fn bulk_revoke_organization_user(
     data: JsonUpcase<Value>,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> Json<Value> {
     let data = data.into_inner().data;
 
@@ -1586,7 +1799,7 @@ async fn bulk_revoke_organization_user(
         Some(org_users) => {
             for org_user_id in org_users {
                 let org_user_id = org_user_id.as_str().unwrap_or_default();
-                let err_msg = match _revoke_organization_user(&org_id, org_user_id, &headers, &mut conn).await {
+                let err_msg = match _revoke_organization_user(&org_id, org_user_id, &headers, &mut conn, &ip).await {
                     Ok(_) => String::new(),
                     Err(e) => format!("{:?}", e),
                 };
@@ -1615,6 +1828,7 @@ async fn _revoke_organization_user(
     org_user_id: &str,
     headers: &AdminHeaders,
     conn: &mut DbConn,
+    ip: &ClientIp,
 ) -> EmptyResult {
     match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await {
         Some(mut user_org) if user_org.status > UserOrgStatus::Revoked as i32 => {
@@ -1632,6 +1846,17 @@ async fn _revoke_organization_user(
 
             user_org.revoke();
             user_org.save(conn).await?;
+
+            log_event(
+                EventType::OrganizationUserRevoked as i32,
+                &user_org.uuid,
+                org_id.to_string(),
+                headers.user.uuid.clone(),
+                headers.device.atype,
+                &ip.ip,
+                conn,
+            )
+            .await;
         }
         Some(_) => err!("User is already revoked"),
         None => err!("User not found in organization"),
@@ -1646,8 +1871,9 @@ async fn activate_organization_user(
     org_user_id: String,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn).await
+    _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await
 }
 
 // Pre web-vault v2022.9.x endpoint
@@ -1657,8 +1883,9 @@ async fn bulk_activate_organization_user(
     data: JsonUpcase<Value>,
     headers: AdminHeaders,
     conn: DbConn,
+    ip: ClientIp,
 ) -> Json<Value> {
-    bulk_restore_organization_user(org_id, data, headers, conn).await
+    bulk_restore_organization_user(org_id, data, headers, conn, ip).await
 }
 
 #[put("/organizations/<org_id>/users/<org_user_id>/restore")]
@@ -1667,8 +1894,9 @@ async fn restore_organization_user(
     org_user_id: String,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn).await
+    _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await
 }
 
 #[put("/organizations/<org_id>/users/restore", data = "<data>")]
@@ -1677,6 +1905,7 @@ async fn bulk_restore_organization_user(
     data: JsonUpcase<Value>,
     headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> Json<Value> {
     let data = data.into_inner().data;
 
@@ -1685,7 +1914,7 @@ async fn bulk_restore_organization_user(
         Some(org_users) => {
             for org_user_id in org_users {
                 let org_user_id = org_user_id.as_str().unwrap_or_default();
-                let err_msg = match _restore_organization_user(&org_id, org_user_id, &headers, &mut conn).await {
+                let err_msg = match _restore_organization_user(&org_id, org_user_id, &headers, &mut conn, &ip).await {
                     Ok(_) => String::new(),
                     Err(e) => format!("{:?}", e),
                 };
@@ -1714,6 +1943,7 @@ async fn _restore_organization_user(
     org_user_id: &str,
     headers: &AdminHeaders,
     conn: &mut DbConn,
+    ip: &ClientIp,
 ) -> EmptyResult {
     match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await {
         Some(mut user_org) if user_org.status < UserOrgStatus::Accepted as i32 => {
@@ -1740,6 +1970,17 @@ async fn _restore_organization_user(
 
             user_org.restore();
             user_org.save(conn).await?;
+
+            log_event(
+                EventType::OrganizationUserRestored as i32,
+                &user_org.uuid,
+                org_id.to_string(),
+                headers.user.uuid.clone(),
+                headers.device.atype,
+                &ip.ip,
+                conn,
+            )
+            .await;
         }
         Some(_) => err!("User is already active"),
         None => err!("User not found in organization"),
@@ -1828,37 +2069,51 @@ impl SelectionReadOnly {
     }
 }
 
-#[post("/organizations/<_org_id>/groups/<group_id>", data = "<data>")]
+#[post("/organizations/<org_id>/groups/<group_id>", data = "<data>")]
 async fn post_group(
-    _org_id: String,
+    org_id: String,
     group_id: String,
     data: JsonUpcase<GroupRequest>,
-    _headers: AdminHeaders,
+    headers: AdminHeaders,
     conn: DbConn,
+    ip: ClientIp,
 ) -> JsonResult {
-    put_group(_org_id, group_id, data, _headers, conn).await
+    put_group(org_id, group_id, data, headers, conn, ip).await
 }
 
 #[post("/organizations/<org_id>/groups", data = "<data>")]
 async fn post_groups(
     org_id: String,
-    _headers: AdminHeaders,
+    headers: AdminHeaders,
     data: JsonUpcase<GroupRequest>,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> JsonResult {
     let group_request = data.into_inner().data;
     let group = group_request.to_group(&org_id)?;
 
+    log_event(
+        EventType::GroupCreated as i32,
+        &group.uuid,
+        org_id,
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     add_update_group(group, group_request.Collections, &mut conn).await
 }
 
-#[put("/organizations/<_org_id>/groups/<group_id>", data = "<data>")]
+#[put("/organizations/<org_id>/groups/<group_id>", data = "<data>")]
 async fn put_group(
-    _org_id: String,
+    org_id: String,
     group_id: String,
     data: JsonUpcase<GroupRequest>,
-    _headers: AdminHeaders,
+    headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> JsonResult {
     let group = match Group::find_by_uuid(&group_id, &mut conn).await {
         Some(group) => group,
@@ -1870,6 +2125,17 @@ async fn put_group(
 
     CollectionGroup::delete_all_by_group(&group_id, &mut conn).await?;
 
+    log_event(
+        EventType::GroupUpdated as i32,
+        &updated_group.uuid,
+        org_id,
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     add_update_group(updated_group, group_request.Collections, &mut conn).await
 }
 
@@ -1915,17 +2181,40 @@ async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHea
 }
 
 #[post("/organizations/<org_id>/groups/<group_id>/delete")]
-async fn post_delete_group(org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
-    delete_group(org_id, group_id, _headers, conn).await
+async fn post_delete_group(
+    org_id: String,
+    group_id: String,
+    headers: AdminHeaders,
+    conn: DbConn,
+    ip: ClientIp,
+) -> EmptyResult {
+    delete_group(org_id, group_id, headers, conn, ip).await
 }
 
-#[delete("/organizations/<_org_id>/groups/<group_id>")]
-async fn delete_group(_org_id: String, group_id: String, _headers: AdminHeaders, mut conn: DbConn) -> EmptyResult {
+#[delete("/organizations/<org_id>/groups/<group_id>")]
+async fn delete_group(
+    org_id: String,
+    group_id: String,
+    headers: AdminHeaders,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> EmptyResult {
     let group = match Group::find_by_uuid(&group_id, &mut conn).await {
         Some(group) => group,
         _ => err!("Group not found"),
     };
 
+    log_event(
+        EventType::GroupDeleted as i32,
+        &group.uuid,
+        org_id,
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     group.delete(&mut conn).await
 }
 
@@ -1955,13 +2244,14 @@ async fn get_group_users(_org_id: String, group_id: String, _headers: AdminHeade
     Ok(Json(json!(group_users)))
 }
 
-#[put("/organizations/<_org_id>/groups/<group_id>/users", data = "<data>")]
+#[put("/organizations/<org_id>/groups/<group_id>/users", data = "<data>")]
 async fn put_group_users(
-    _org_id: String,
+    org_id: String,
     group_id: String,
-    _headers: AdminHeaders,
+    headers: AdminHeaders,
     data: JsonVec<String>,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
     match Group::find_by_uuid(&group_id, &mut conn).await {
         Some(_) => { /* Do nothing */ }
@@ -1972,8 +2262,19 @@ async fn put_group_users(
 
     let assigned_user_ids = data.into_inner();
     for assigned_user_id in assigned_user_ids {
-        let mut user_entry = GroupUser::new(group_id.clone(), assigned_user_id);
+        let mut user_entry = GroupUser::new(group_id.clone(), assigned_user_id.clone());
         user_entry.save(&mut conn).await?;
+
+        log_event(
+            EventType::OrganizationUserUpdatedGroups as i32,
+            &assigned_user_id,
+            org_id.clone(),
+            headers.user.uuid.clone(),
+            headers.device.atype,
+            &ip.ip,
+            &mut conn,
+        )
+        .await;
     }
 
     Ok(())
@@ -1998,61 +2299,76 @@ struct OrganizationUserUpdateGroupsRequest {
     GroupIds: Vec<String>,
 }
 
-#[post("/organizations/<_org_id>/users/<user_id>/groups", data = "<data>")]
+#[post("/organizations/<org_id>/users/<org_user_id>/groups", data = "<data>")]
 async fn post_user_groups(
-    _org_id: String,
-    user_id: String,
+    org_id: String,
+    org_user_id: String,
     data: JsonUpcase<OrganizationUserUpdateGroupsRequest>,
-    _headers: AdminHeaders,
+    headers: AdminHeaders,
     conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    put_user_groups(_org_id, user_id, data, _headers, conn).await
+    put_user_groups(org_id, org_user_id, data, headers, conn, ip).await
 }
 
-#[put("/organizations/<_org_id>/users/<user_id>/groups", data = "<data>")]
+#[put("/organizations/<org_id>/users/<org_user_id>/groups", data = "<data>")]
 async fn put_user_groups(
-    _org_id: String,
-    user_id: String,
+    org_id: String,
+    org_user_id: String,
     data: JsonUpcase<OrganizationUserUpdateGroupsRequest>,
-    _headers: AdminHeaders,
+    headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    match UserOrganization::find_by_uuid(&user_id, &mut conn).await {
+    match UserOrganization::find_by_uuid(&org_user_id, &mut conn).await {
         Some(_) => { /* Do nothing */ }
         _ => err!("User could not be found!"),
     };
 
-    GroupUser::delete_all_by_user(&user_id, &mut conn).await?;
+    GroupUser::delete_all_by_user(&org_user_id, &mut conn).await?;
 
     let assigned_group_ids = data.into_inner().data;
     for assigned_group_id in assigned_group_ids.GroupIds {
-        let mut group_user = GroupUser::new(assigned_group_id.clone(), user_id.clone());
+        let mut group_user = GroupUser::new(assigned_group_id.clone(), org_user_id.clone());
         group_user.save(&mut conn).await?;
     }
 
+    log_event(
+        EventType::OrganizationUserUpdatedGroups as i32,
+        &org_user_id,
+        org_id,
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
     Ok(())
 }
 
-#[post("/organizations/<org_id>/groups/<group_id>/delete-user/<user_id>")]
+#[post("/organizations/<org_id>/groups/<group_id>/delete-user/<org_user_id>")]
 async fn post_delete_group_user(
     org_id: String,
     group_id: String,
-    user_id: String,
+    org_user_id: String,
     headers: AdminHeaders,
     conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    delete_group_user(org_id, group_id, user_id, headers, conn).await
+    delete_group_user(org_id, group_id, org_user_id, headers, conn, ip).await
 }
 
-#[delete("/organizations/<_org_id>/groups/<group_id>/users/<user_id>")]
+#[delete("/organizations/<org_id>/groups/<group_id>/users/<org_user_id>")]
 async fn delete_group_user(
-    _org_id: String,
+    org_id: String,
     group_id: String,
-    user_id: String,
-    _headers: AdminHeaders,
+    org_user_id: String,
+    headers: AdminHeaders,
     mut conn: DbConn,
+    ip: ClientIp,
 ) -> EmptyResult {
-    match UserOrganization::find_by_uuid(&user_id, &mut conn).await {
+    match UserOrganization::find_by_uuid(&org_user_id, &mut conn).await {
         Some(_) => { /* Do nothing */ }
         _ => err!("User could not be found!"),
     };
@@ -2062,7 +2378,18 @@ async fn delete_group_user(
         _ => err!("Group could not be found!"),
     };
 
-    GroupUser::delete_by_group_id_and_user_id(&group_id, &user_id, &mut conn).await
+    log_event(
+        EventType::OrganizationUserUpdatedGroups as i32,
+        &org_user_id,
+        org_id,
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
+    GroupUser::delete_by_group_id_and_user_id(&group_id, &org_user_id, &mut conn).await
 }
 
 // This is a new function active since the v2022.9.x clients.
diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs
index 68342e95..fa1792f2 100644
--- a/src/api/core/two_factor/authenticator.rs
+++ b/src/api/core/two_factor/authenticator.rs
@@ -4,12 +4,13 @@ use rocket::Route;
 
 use crate::{
     api::{
-        core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData,
+        core::log_user_event, core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase,
+        NumberOrString, PasswordData,
     },
     auth::{ClientIp, Headers},
     crypto,
     db::{
-        models::{TwoFactor, TwoFactorType},
+        models::{EventType, TwoFactor, TwoFactorType},
         DbConn,
     },
 };
@@ -85,6 +86,8 @@ async fn activate_authenticator(
 
     _generate_recover_code(&mut user, &mut conn).await;
 
+    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
+
     Ok(Json(json!({
         "Enabled": true,
         "Key": key,
@@ -167,10 +170,20 @@ pub async fn validate_totp_code(
             return Ok(());
         } else if generated == totp_code && time_step <= i64::from(twofactor.last_used) {
             warn!("This TOTP or a TOTP code within {} steps back or forward has already been used!", steps);
-            err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip));
+            err!(
+                format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip),
+                ErrorEvent {
+                    event: EventType::UserFailedLogIn2fa
+                }
+            );
         }
     }
 
     // Else no valide code received, deny access
-    err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip));
+    err!(
+        format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip),
+        ErrorEvent {
+            event: EventType::UserFailedLogIn2fa
+        }
+    );
 }
diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs
index 42cc709e..06210d23 100644
--- a/src/api/core/two_factor/duo.rs
+++ b/src/api/core/two_factor/duo.rs
@@ -4,11 +4,14 @@ use rocket::serde::json::Json;
 use rocket::Route;
 
 use crate::{
-    api::{core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData},
-    auth::Headers,
+    api::{
+        core::log_user_event, core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase,
+        PasswordData,
+    },
+    auth::{ClientIp, Headers},
     crypto,
     db::{
-        models::{TwoFactor, TwoFactorType, User},
+        models::{EventType, TwoFactor, TwoFactorType, User},
         DbConn,
     },
     error::MapResult,
@@ -152,7 +155,7 @@ fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
 }
 
 #[post("/two-factor/duo", data = "<data>")]
-async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut conn: DbConn) -> JsonResult {
+async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut conn: DbConn, ip: ClientIp) -> JsonResult {
     let data: EnableDuoData = data.into_inner().data;
     let mut user = headers.user;
 
@@ -175,6 +178,8 @@ async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut con
 
     _generate_recover_code(&mut user, &mut conn).await;
 
+    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
+
     Ok(Json(json!({
         "Enabled": true,
         "Host": data.host,
@@ -185,8 +190,8 @@ async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut con
 }
 
 #[put("/two-factor/duo", data = "<data>")]
-async fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_duo(data, headers, conn).await
+async fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn, ip: ClientIp) -> JsonResult {
+    activate_duo(data, headers, conn, ip).await
 }
 
 async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
@@ -282,7 +287,12 @@ pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn)
 
     let split: Vec<&str> = response.split(':').collect();
     if split.len() != 2 {
-        err!("Invalid response length");
+        err!(
+            "Invalid response length",
+            ErrorEvent {
+                event: EventType::UserFailedLogIn2fa
+            }
+        );
     }
 
     let auth_sig = split[0];
@@ -296,7 +306,12 @@ pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn)
     let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;
 
     if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) {
-        err!("Error validating duo authentication")
+        err!(
+            "Error validating duo authentication",
+            ErrorEvent {
+                event: EventType::UserFailedLogIn2fa
+            }
+        )
     }
 
     Ok(())
diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs
index 90247f53..9a95c465 100644
--- a/src/api/core/two_factor/email.rs
+++ b/src/api/core/two_factor/email.rs
@@ -3,11 +3,14 @@ use rocket::serde::json::Json;
 use rocket::Route;
 
 use crate::{
-    api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData},
-    auth::Headers,
+    api::{
+        core::{log_user_event, two_factor::_generate_recover_code},
+        EmptyResult, JsonResult, JsonUpcase, PasswordData,
+    },
+    auth::{ClientIp, Headers},
     crypto,
     db::{
-        models::{TwoFactor, TwoFactorType},
+        models::{EventType, TwoFactor, TwoFactorType},
         DbConn,
     },
     error::{Error, MapResult},
@@ -147,7 +150,7 @@ struct EmailData {
 
 /// Verify email belongs to user and can be used for 2FA email codes.
 #[put("/two-factor/email", data = "<data>")]
-async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn) -> JsonResult {
+async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn, ip: ClientIp) -> JsonResult {
     let data: EmailData = data.into_inner().data;
     let mut user = headers.user;
 
@@ -177,6 +180,8 @@ async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn)
 
     _generate_recover_code(&mut user, &mut conn).await;
 
+    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
+
     Ok(Json(json!({
         "Email": email_data.email,
         "Enabled": "true",
@@ -192,7 +197,12 @@ pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, c
         .map_res("Two factor not found")?;
     let issued_token = match &email_data.last_token {
         Some(t) => t,
-        _ => err!("No token available"),
+        _ => err!(
+            "No token available",
+            ErrorEvent {
+                event: EventType::UserFailedLogIn2fa
+            }
+        ),
     };
 
     if !crypto::ct_eq(issued_token, token) {
@@ -203,21 +213,32 @@ pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, c
         twofactor.data = email_data.to_json();
         twofactor.save(conn).await?;
 
-        err!("Token is invalid")
+        err!(
+            "Token is invalid",
+            ErrorEvent {
+                event: EventType::UserFailedLogIn2fa
+            }
+        )
     }
 
     email_data.reset_token();
     twofactor.data = email_data.to_json();
     twofactor.save(conn).await?;
 
-    let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0);
+    let date = NaiveDateTime::from_timestamp_opt(email_data.token_sent, 0).expect("Email token timestamp invalid.");
     let max_time = CONFIG.email_expiration_time() as i64;
     if date + Duration::seconds(max_time) < Utc::now().naive_utc() {
-        err!("Token has expired")
+        err!(
+            "Token has expired",
+            ErrorEvent {
+                event: EventType::UserFailedLogIn2fa
+            }
+        )
     }
 
     Ok(())
 }
+
 /// Data stored in the TwoFactor table in the db
 #[derive(Serialize, Deserialize)]
 pub struct EmailTokenData {
diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs
index 3d5eee83..ce3cfb72 100644
--- a/src/api/core/two_factor/mod.rs
+++ b/src/api/core/two_factor/mod.rs
@@ -5,8 +5,8 @@ use rocket::Route;
 use serde_json::Value;
 
 use crate::{
-    api::{JsonResult, JsonUpcase, NumberOrString, PasswordData},
-    auth::Headers,
+    api::{core::log_user_event, JsonResult, JsonUpcase, NumberOrString, PasswordData},
+    auth::{ClientIp, Headers},
     crypto,
     db::{models::*, DbConn, DbPool},
     mail, CONFIG,
@@ -73,7 +73,7 @@ struct RecoverTwoFactor {
 }
 
 #[post("/two-factor/recover", data = "<data>")]
-async fn recover(data: JsonUpcase<RecoverTwoFactor>, mut conn: DbConn) -> JsonResult {
+async fn recover(data: JsonUpcase<RecoverTwoFactor>, headers: Headers, mut conn: DbConn, ip: ClientIp) -> JsonResult {
     let data: RecoverTwoFactor = data.into_inner().data;
 
     use crate::db::models::User;
@@ -97,6 +97,8 @@ async fn recover(data: JsonUpcase<RecoverTwoFactor>, mut conn: DbConn) -> JsonRe
     // Remove all twofactors from the user
     TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
 
+    log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
+
     // Remove the recovery code, not needed without twofactors
     user.totp_recover = None;
     user.save(&mut conn).await?;
@@ -119,7 +121,12 @@ struct DisableTwoFactorData {
 }
 
 #[post("/two-factor/disable", data = "<data>")]
-async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, mut conn: DbConn) -> JsonResult {
+async fn disable_twofactor(
+    data: JsonUpcase<DisableTwoFactorData>,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> JsonResult {
     let data: DisableTwoFactorData = data.into_inner().data;
     let password_hash = data.MasterPasswordHash;
     let user = headers.user;
@@ -132,6 +139,7 @@ async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Head
 
     if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
         twofactor.delete(&mut conn).await?;
+        log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
     }
 
     let twofactor_disabled = TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty();
@@ -160,8 +168,13 @@ async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Head
 }
 
 #[put("/two-factor/disable", data = "<data>")]
-async fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
-    disable_twofactor(data, headers, conn).await
+async fn disable_twofactor_put(
+    data: JsonUpcase<DisableTwoFactorData>,
+    headers: Headers,
+    conn: DbConn,
+    ip: ClientIp,
+) -> JsonResult {
+    disable_twofactor(data, headers, conn, ip).await
 }
 
 pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs
index 0d9e5542..97711c75 100644
--- a/src/api/core/two_factor/webauthn.rs
+++ b/src/api/core/two_factor/webauthn.rs
@@ -6,11 +6,12 @@ use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState,
 
 use crate::{
     api::{
-        core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData,
+        core::{log_user_event, two_factor::_generate_recover_code},
+        EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData,
     },
-    auth::Headers,
+    auth::{ClientIp, Headers},
     db::{
-        models::{TwoFactor, TwoFactorType},
+        models::{EventType, TwoFactor, TwoFactorType},
         DbConn,
     },
     error::Error,
@@ -241,7 +242,12 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
 }
 
 #[post("/two-factor/webauthn", data = "<data>")]
-async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
+async fn activate_webauthn(
+    data: JsonUpcase<EnableWebauthnData>,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> JsonResult {
     let data: EnableWebauthnData = data.into_inner().data;
     let mut user = headers.user;
 
@@ -280,6 +286,8 @@ async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Header
         .await?;
     _generate_recover_code(&mut user, &mut conn).await;
 
+    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
+
     let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
     Ok(Json(json!({
         "Enabled": true,
@@ -289,8 +297,13 @@ async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Header
 }
 
 #[put("/two-factor/webauthn", data = "<data>")]
-async fn activate_webauthn_put(data: JsonUpcase<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_webauthn(data, headers, conn).await
+async fn activate_webauthn_put(
+    data: JsonUpcase<EnableWebauthnData>,
+    headers: Headers,
+    conn: DbConn,
+    ip: ClientIp,
+) -> JsonResult {
+    activate_webauthn(data, headers, conn, ip).await
 }
 
 #[derive(Deserialize, Debug)]
@@ -391,7 +404,12 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut
             tf.delete(conn).await?;
             state
         }
-        None => err!("Can't recover login challenge"),
+        None => err!(
+            "Can't recover login challenge",
+            ErrorEvent {
+                event: EventType::UserFailedLogIn2fa
+            }
+        ),
     };
 
     let rsp: crate::util::UpCase<PublicKeyCredentialCopy> = serde_json::from_str(response)?;
@@ -414,5 +432,10 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut
         }
     }
 
-    err!("Credential not present")
+    err!(
+        "Credential not present",
+        ErrorEvent {
+            event: EventType::UserFailedLogIn2fa
+        }
+    )
 }
diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs
index 7994bea0..00ef7df2 100644
--- a/src/api/core/two_factor/yubikey.rs
+++ b/src/api/core/two_factor/yubikey.rs
@@ -4,10 +4,13 @@ use serde_json::Value;
 use yubico::{config::Config, verify};
 
 use crate::{
-    api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData},
-    auth::Headers,
+    api::{
+        core::{log_user_event, two_factor::_generate_recover_code},
+        EmptyResult, JsonResult, JsonUpcase, PasswordData,
+    },
+    auth::{ClientIp, Headers},
     db::{
-        models::{TwoFactor, TwoFactorType},
+        models::{EventType, TwoFactor, TwoFactorType},
         DbConn,
     },
     error::{Error, MapResult},
@@ -113,7 +116,12 @@ async fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, mut
 }
 
 #[post("/two-factor/yubikey", data = "<data>")]
-async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, mut conn: DbConn) -> JsonResult {
+async fn activate_yubikey(
+    data: JsonUpcase<EnableYubikeyData>,
+    headers: Headers,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> JsonResult {
     let data: EnableYubikeyData = data.into_inner().data;
     let mut user = headers.user;
 
@@ -159,6 +167,8 @@ async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers,
 
     _generate_recover_code(&mut user, &mut conn).await;
 
+    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
+
     let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
 
     result["Enabled"] = Value::Bool(true);
@@ -169,8 +179,13 @@ async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers,
 }
 
 #[put("/two-factor/yubikey", data = "<data>")]
-async fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_yubikey(data, headers, conn).await
+async fn activate_yubikey_put(
+    data: JsonUpcase<EnableYubikeyData>,
+    headers: Headers,
+    conn: DbConn,
+    ip: ClientIp,
+) -> JsonResult {
+    activate_yubikey(data, headers, conn, ip).await
 }
 
 pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
diff --git a/src/api/identity.rs b/src/api/identity.rs
index 9e747c7d..6499ee38 100644
--- a/src/api/identity.rs
+++ b/src/api/identity.rs
@@ -10,6 +10,7 @@ use serde_json::Value;
 use crate::{
     api::{
         core::accounts::{PreloginData, RegisterData, _prelogin, _register},
+        core::log_user_event,
         core::two_factor::{duo, email, email::EmailTokenData, yubikey},
         ApiResult, EmptyResult, JsonResult, JsonUpcase,
     },
@@ -24,13 +25,16 @@ pub fn routes() -> Vec<Route> {
 }
 
 #[post("/connect/token", data = "<data>")]
-async fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult {
+async fn login(data: Form<ConnectData>, mut conn: DbConn, ip: ClientIp) -> JsonResult {
     let data: ConnectData = data.into_inner();
 
-    match data.grant_type.as_ref() {
+    let mut user_uuid: Option<String> = None;
+    let device_type = data.device_type.clone();
+
+    let login_result = match data.grant_type.as_ref() {
         "refresh_token" => {
             _check_is_some(&data.refresh_token, "refresh_token cannot be blank")?;
-            _refresh_login(data, conn).await
+            _refresh_login(data, &mut conn).await
         }
         "password" => {
             _check_is_some(&data.client_id, "client_id cannot be blank")?;
@@ -42,34 +46,51 @@ async fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResul
             _check_is_some(&data.device_name, "device_name cannot be blank")?;
             _check_is_some(&data.device_type, "device_type cannot be blank")?;
 
-            _password_login(data, conn, &ip).await
+            _password_login(data, &mut user_uuid, &mut conn, &ip).await
         }
         "client_credentials" => {
             _check_is_some(&data.client_id, "client_id cannot be blank")?;
             _check_is_some(&data.client_secret, "client_secret cannot be blank")?;
             _check_is_some(&data.scope, "scope cannot be blank")?;
 
-            _api_key_login(data, conn, &ip).await
+            _api_key_login(data, &mut user_uuid, &mut conn, &ip).await
         }
         t => err!("Invalid type", t),
+    };
+
+    if let Some(user_uuid) = user_uuid {
+        // When unknown or unable to parse, return 14, which is 'Unknown Browser'
+        let device_type = util::try_parse_string(device_type).unwrap_or(14);
+        match &login_result {
+            Ok(_) => {
+                log_user_event(EventType::UserLoggedIn as i32, &user_uuid, device_type, &ip.ip, &mut conn).await;
+            }
+            Err(e) => {
+                if let Some(ev) = e.get_event() {
+                    log_user_event(ev.event as i32, &user_uuid, device_type, &ip.ip, &mut conn).await
+                }
+            }
+        }
     }
+
+    login_result
 }
 
-async fn _refresh_login(data: ConnectData, mut conn: DbConn) -> JsonResult {
+async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
     // Extract token
     let token = data.refresh_token.unwrap();
 
     // Get device by refresh token
-    let mut device = Device::find_by_refresh_token(&token, &mut conn).await.map_res("Invalid refresh token")?;
+    let mut device = Device::find_by_refresh_token(&token, conn).await.map_res("Invalid refresh token")?;
 
     let scope = "api offline_access";
     let scope_vec = vec!["api".into(), "offline_access".into()];
 
     // Common
-    let user = User::find_by_uuid(&device.user_uuid, &mut conn).await.unwrap();
-    let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &mut conn).await;
+    let user = User::find_by_uuid(&device.user_uuid, conn).await.unwrap();
+    let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
     let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
-    device.save(&mut conn).await?;
+    device.save(conn).await?;
 
     Ok(Json(json!({
         "access_token": access_token,
@@ -87,7 +108,12 @@ async fn _refresh_login(data: ConnectData, mut conn: DbConn) -> JsonResult {
     })))
 }
 
-async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> JsonResult {
+async fn _password_login(
+    data: ConnectData,
+    user_uuid: &mut Option<String>,
+    conn: &mut DbConn,
+    ip: &ClientIp,
+) -> JsonResult {
     // Validate scope
     let scope = data.scope.as_ref().unwrap();
     if scope != "api offline_access" {
@@ -100,20 +126,35 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) ->
 
     // Get the user
     let username = data.username.as_ref().unwrap().trim();
-    let user = match User::find_by_mail(username, &mut conn).await {
+    let user = match User::find_by_mail(username, conn).await {
         Some(user) => user,
         None => err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)),
     };
 
+    // Set the user_uuid here to be passed back used for event logging.
+    *user_uuid = Some(user.uuid.clone());
+
     // Check password
     let password = data.password.as_ref().unwrap();
     if !user.check_valid_password(password) {
-        err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username))
+        err!(
+            "Username or password is incorrect. Try again",
+            format!("IP: {}. Username: {}.", ip.ip, username),
+            ErrorEvent {
+                event: EventType::UserFailedLogIn,
+            }
+        )
     }
 
     // Check if the user is disabled
     if !user.enabled {
-        err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username))
+        err!(
+            "This user has been disabled",
+            format!("IP: {}. Username: {}.", ip.ip, username),
+            ErrorEvent {
+                event: EventType::UserFailedLogIn
+            }
+        )
     }
 
     let now = Utc::now().naive_utc();
@@ -131,7 +172,7 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) ->
                 user.last_verifying_at = Some(now);
                 user.login_verify_count += 1;
 
-                if let Err(e) = user.save(&mut conn).await {
+                if let Err(e) = user.save(conn).await {
                     error!("Error updating user: {:#?}", e);
                 }
 
@@ -142,27 +183,38 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) ->
         }
 
         // 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))
+        err!(
+            "Please verify your email before trying again.",
+            format!("IP: {}. Username: {}.", ip.ip, username),
+            ErrorEvent {
+                event: EventType::UserFailedLogIn
+            }
+        )
     }
 
-    let (mut device, new_device) = get_device(&data, &mut conn, &user).await;
+    let (mut device, new_device) = get_device(&data, conn, &user).await;
 
-    let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, &mut conn).await?;
+    let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, conn).await?;
 
     if CONFIG.mail_enabled() && new_device {
         if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await {
             error!("Error sending new device email: {:#?}", e);
 
             if CONFIG.require_device_email() {
-                err!("Could not send login notification email. Please contact your administrator.")
+                err!(
+                    "Could not send login notification email. Please contact your administrator.",
+                    ErrorEvent {
+                        event: EventType::UserFailedLogIn
+                    }
+                )
             }
         }
     }
 
     // Common
-    let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &mut conn).await;
+    let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
     let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
-    device.save(&mut conn).await?;
+    device.save(conn).await?;
 
     let mut result = json!({
         "access_token": access_token,
@@ -188,7 +240,12 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) ->
     Ok(Json(result))
 }
 
-async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> JsonResult {
+async fn _api_key_login(
+    data: ConnectData,
+    user_uuid: &mut Option<String>,
+    conn: &mut DbConn,
+    ip: &ClientIp,
+) -> JsonResult {
     // Validate scope
     let scope = data.scope.as_ref().unwrap();
     if scope != "api" {
@@ -201,27 +258,42 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J
 
     // Get the user via the client_id
     let client_id = data.client_id.as_ref().unwrap();
-    let user_uuid = match client_id.strip_prefix("user.") {
+    let client_user_uuid = match client_id.strip_prefix("user.") {
         Some(uuid) => uuid,
         None => err!("Malformed client_id", format!("IP: {}.", ip.ip)),
     };
-    let user = match User::find_by_uuid(user_uuid, &mut conn).await {
+    let user = match User::find_by_uuid(client_user_uuid, conn).await {
         Some(user) => user,
         None => err!("Invalid client_id", format!("IP: {}.", ip.ip)),
     };
 
+    // Set the user_uuid here to be passed back used for event logging.
+    *user_uuid = Some(user.uuid.clone());
+
     // Check if the user is disabled
     if !user.enabled {
-        err!("This user has been disabled (API key login)", format!("IP: {}. Username: {}.", ip.ip, user.email))
+        err!(
+            "This user has been disabled (API key login)",
+            format!("IP: {}. Username: {}.", ip.ip, user.email),
+            ErrorEvent {
+                event: EventType::UserFailedLogIn
+            }
+        )
     }
 
     // Check API key. Note that API key logins bypass 2FA.
     let client_secret = data.client_secret.as_ref().unwrap();
     if !user.check_valid_api_key(client_secret) {
-        err!("Incorrect client_secret", format!("IP: {}. Username: {}.", ip.ip, user.email))
+        err!(
+            "Incorrect client_secret",
+            format!("IP: {}. Username: {}.", ip.ip, user.email),
+            ErrorEvent {
+                event: EventType::UserFailedLogIn
+            }
+        )
     }
 
-    let (mut device, new_device) = get_device(&data, &mut conn, &user).await;
+    let (mut device, new_device) = get_device(&data, conn, &user).await;
 
     if CONFIG.mail_enabled() && new_device {
         let now = Utc::now().naive_utc();
@@ -229,15 +301,20 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J
             error!("Error sending new device email: {:#?}", e);
 
             if CONFIG.require_device_email() {
-                err!("Could not send login notification email. Please contact your administrator.")
+                err!(
+                    "Could not send login notification email. Please contact your administrator.",
+                    ErrorEvent {
+                        event: EventType::UserFailedLogIn
+                    }
+                )
             }
         }
     }
 
     // Common
-    let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &mut conn).await;
+    let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
     let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
-    device.save(&mut conn).await?;
+    device.save(conn).await?;
 
     info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
 
@@ -261,7 +338,8 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J
 /// Retrieves an existing device or creates a new device from ConnectData and the User
 async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> (Device, bool) {
     // On iOS, device_type sends "iOS", on others it sends a number
-    let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(0);
+    // When unknown or unable to parse, return 14, which is 'Unknown Browser'
+    let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(14);
     let device_id = data.device_identifier.clone().expect("No device id provided");
     let device_name = data.device_name.clone().expect("No device name provided");
 
@@ -338,7 +416,12 @@ async fn twofactor_auth(
                 }
             }
         }
-        _ => err!("Invalid two factor provider"),
+        _ => err!(
+            "Invalid two factor provider",
+            ErrorEvent {
+                event: EventType::UserFailedLogIn2fa
+            }
+        ),
     }
 
     TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn).await?;
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 49283dd2..0861ea2d 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -16,6 +16,7 @@ pub use crate::api::{
     core::routes as core_routes,
     core::two_factor::send_incomplete_2fa_notifications,
     core::{emergency_notification_reminder_job, emergency_request_timeout_job},
+    core::{event_cleanup_job, events_routes as core_events_routes},
     icons::routes as icons_routes,
     identity::routes as identity_routes,
     notifications::routes as notifications_routes,
diff --git a/src/config.rs b/src/config.rs
index 4aa8d649..fe98d2df 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -371,6 +371,9 @@ make_config! {
         /// Emergency request timeout schedule |> Cron schedule of the job that grants emergency access requests that have met the required wait time.
         /// Defaults to hourly. Set blank to disable this job.
         emergency_request_timeout_schedule:   String, false,  def,    "0 5 * * * *".to_string();
+        /// Event cleanup schedule |> Cron schedule of the job that cleans old events from the event table.
+        /// Defaults to daily. Set blank to disable this job.
+        event_cleanup_schedule:   String, false,  def,    "0 10 0 * * *".to_string();
     },
 
     /// General settings
@@ -426,6 +429,8 @@ make_config! {
         signups_verify_resend_limit: u32, true, def,    6;
         /// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled
         signups_domains_whitelist: String, true, def,   String::new();
+        /// Enable event logging |> Enables event logging for organizations.
+        org_events_enabled:     bool,   false,  def,    false;
         /// Org creation users |> Allow org creation only by this list of comma-separated user emails.
         /// Blank or 'all' means all users can create orgs; 'none' means no users can create orgs.
         org_creation_users:     String, true,   def,    String::new();
@@ -451,6 +456,9 @@ make_config! {
 
         /// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
         invitation_org_name:    String, true,   def,    "Vaultwarden".to_string();
+
+        /// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefently.
+        events_days_retain:     i64,    false,   option;
     },
 
     /// Advanced settings
@@ -746,26 +754,35 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
         err!("`INVITATION_EXPIRATION_HOURS` has a minimum duration of 1 hour")
     }
 
+    // Validate schedule crontab format
     if !cfg.send_purge_schedule.is_empty() && cfg.send_purge_schedule.parse::<Schedule>().is_err() {
         err!("`SEND_PURGE_SCHEDULE` is not a valid cron expression")
     }
+
     if !cfg.trash_purge_schedule.is_empty() && cfg.trash_purge_schedule.parse::<Schedule>().is_err() {
         err!("`TRASH_PURGE_SCHEDULE` is not a valid cron expression")
     }
+
     if !cfg.incomplete_2fa_schedule.is_empty() && cfg.incomplete_2fa_schedule.parse::<Schedule>().is_err() {
         err!("`INCOMPLETE_2FA_SCHEDULE` is not a valid cron expression")
     }
+
     if !cfg.emergency_notification_reminder_schedule.is_empty()
         && cfg.emergency_notification_reminder_schedule.parse::<Schedule>().is_err()
     {
         err!("`EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE` is not a valid cron expression")
     }
+
     if !cfg.emergency_request_timeout_schedule.is_empty()
         && cfg.emergency_request_timeout_schedule.parse::<Schedule>().is_err()
     {
         err!("`EMERGENCY_REQUEST_TIMEOUT_SCHEDULE` is not a valid cron expression")
     }
 
+    if !cfg.event_cleanup_schedule.is_empty() && cfg.event_cleanup_schedule.parse::<Schedule>().is_err() {
+        err!("`EVENT_CLEANUP_SCHEDULE` is not a valid cron expression")
+    }
+
     Ok(())
 }
 
@@ -1125,7 +1142,7 @@ fn case_helper<'reg, 'rc>(
     let value = param.value().clone();
 
     if h.params().iter().skip(1).any(|x| x.value() == &value) {
-        h.template().map(|t| t.render(r, ctx, rc, out)).unwrap_or(Ok(()))
+        h.template().map(|t| t.render(r, ctx, rc, out)).unwrap_or_else(|| Ok(()))
     } else {
         Ok(())
     }
diff --git a/src/db/models/event.rs b/src/db/models/event.rs
new file mode 100644
index 00000000..9196b8a8
--- /dev/null
+++ b/src/db/models/event.rs
@@ -0,0 +1,318 @@
+use crate::db::DbConn;
+use serde_json::Value;
+
+use crate::{api::EmptyResult, error::MapResult, CONFIG};
+
+use chrono::{Duration, NaiveDateTime, Utc};
+
+// https://bitwarden.com/help/event-logs/
+
+db_object! {
+    // Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
+    // Upstream: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Api/Models/Public/Response/EventResponseModel.cs
+    // Upstream SQL: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Sql/dbo/Tables/Event.sql
+    #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
+    #[diesel(table_name = event)]
+    #[diesel(primary_key(uuid))]
+    pub struct Event {
+        pub uuid: String,
+        pub event_type: i32, // EventType
+        pub user_uuid: Option<String>,
+        pub org_uuid: Option<String>,
+        pub cipher_uuid: Option<String>,
+        pub collection_uuid: Option<String>,
+        pub group_uuid: Option<String>,
+        pub org_user_uuid: Option<String>,
+        pub act_user_uuid: Option<String>,
+        // Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs
+        pub device_type: Option<i32>,
+        pub ip_address: Option<String>,
+        pub event_date: NaiveDateTime,
+        pub policy_uuid: Option<String>,
+        pub provider_uuid: Option<String>,
+        pub provider_user_uuid: Option<String>,
+        pub provider_org_uuid: Option<String>,
+    }
+}
+
+// Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/EventType.cs
+#[derive(Debug, Copy, Clone)]
+pub enum EventType {
+    // User
+    UserLoggedIn = 1000,
+    UserChangedPassword = 1001,
+    UserUpdated2fa = 1002,
+    UserDisabled2fa = 1003,
+    UserRecovered2fa = 1004,
+    UserFailedLogIn = 1005,
+    UserFailedLogIn2fa = 1006,
+    UserClientExportedVault = 1007,
+    // UserUpdatedTempPassword = 1008, // Not supported
+    // UserMigratedKeyToKeyConnector = 1009, // Not supported
+
+    // Cipher
+    CipherCreated = 1100,
+    CipherUpdated = 1101,
+    CipherDeleted = 1102,
+    CipherAttachmentCreated = 1103,
+    CipherAttachmentDeleted = 1104,
+    CipherShared = 1105,
+    CipherUpdatedCollections = 1106,
+    CipherClientViewed = 1107,
+    CipherClientToggledPasswordVisible = 1108,
+    CipherClientToggledHiddenFieldVisible = 1109,
+    CipherClientToggledCardCodeVisible = 1110,
+    CipherClientCopiedPassword = 1111,
+    CipherClientCopiedHiddenField = 1112,
+    CipherClientCopiedCardCode = 1113,
+    CipherClientAutofilled = 1114,
+    CipherSoftDeleted = 1115,
+    CipherRestored = 1116,
+    CipherClientToggledCardNumberVisible = 1117,
+
+    // Collection
+    CollectionCreated = 1300,
+    CollectionUpdated = 1301,
+    CollectionDeleted = 1302,
+
+    // Group
+    GroupCreated = 1400,
+    GroupUpdated = 1401,
+    GroupDeleted = 1402,
+
+    // OrganizationUser
+    OrganizationUserInvited = 1500,
+    OrganizationUserConfirmed = 1501,
+    OrganizationUserUpdated = 1502,
+    OrganizationUserRemoved = 1503,
+    OrganizationUserUpdatedGroups = 1504,
+    // OrganizationUserUnlinkedSso = 1505, // Not supported
+    // OrganizationUserResetPasswordEnroll = 1506, // Not supported
+    // OrganizationUserResetPasswordWithdraw = 1507, // Not supported
+    // OrganizationUserAdminResetPassword = 1508, // Not supported
+    // OrganizationUserResetSsoLink = 1509, // Not supported
+    // OrganizationUserFirstSsoLogin = 1510, // Not supported
+    OrganizationUserRevoked = 1511,
+    OrganizationUserRestored = 1512,
+
+    // Organization
+    OrganizationUpdated = 1600,
+    OrganizationPurgedVault = 1601,
+    OrganizationClientExportedVault = 1602,
+    // OrganizationVaultAccessed = 1603,
+    // OrganizationEnabledSso = 1604, // Not supported
+    // OrganizationDisabledSso = 1605, // Not supported
+    // OrganizationEnabledKeyConnector = 1606, // Not supported
+    // OrganizationDisabledKeyConnector = 1607, // Not supported
+    // OrganizationSponsorshipsSynced = 1608, // Not supported
+
+    // Policy
+    PolicyUpdated = 1700,
+    // Provider (Not yet supported)
+    // ProviderUserInvited = 1800, // Not supported
+    // ProviderUserConfirmed = 1801, // Not supported
+    // ProviderUserUpdated = 1802, // Not supported
+    // ProviderUserRemoved = 1803, // Not supported
+    // ProviderOrganizationCreated = 1900, // Not supported
+    // ProviderOrganizationAdded = 1901, // Not supported
+    // ProviderOrganizationRemoved = 1902, // Not supported
+    // ProviderOrganizationVaultAccessed = 1903, // Not supported
+}
+
+/// Local methods
+impl Event {
+    pub fn new(event_type: i32, event_date: Option<NaiveDateTime>) -> Self {
+        let event_date = match event_date {
+            Some(d) => d,
+            None => Utc::now().naive_utc(),
+        };
+
+        Self {
+            uuid: crate::util::get_uuid(),
+            event_type,
+            user_uuid: None,
+            org_uuid: None,
+            cipher_uuid: None,
+            collection_uuid: None,
+            group_uuid: None,
+            org_user_uuid: None,
+            act_user_uuid: None,
+            device_type: None,
+            ip_address: None,
+            event_date,
+            policy_uuid: None,
+            provider_uuid: None,
+            provider_user_uuid: None,
+            provider_org_uuid: None,
+        }
+    }
+
+    pub fn to_json(&self) -> Value {
+        use crate::util::format_date;
+
+        json!({
+            "type": self.event_type,
+            "userId": self.user_uuid,
+            "organizationId": self.org_uuid,
+            "cipherId": self.cipher_uuid,
+            "collectionId": self.collection_uuid,
+            "groupId": self.group_uuid,
+            "organizationUserId": self.org_user_uuid,
+            "actingUserId": self.act_user_uuid,
+            "date": format_date(&self.event_date),
+            "deviceType": self.device_type,
+            "ipAddress": self.ip_address,
+            "policyId": self.policy_uuid,
+            "providerId": self.provider_uuid,
+            "providerUserId": self.provider_user_uuid,
+            "providerOrganizationId": self.provider_org_uuid,
+            // "installationId": null, // Not supported
+        })
+    }
+}
+
+/// Database methods
+/// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs
+impl Event {
+    pub const PAGE_SIZE: i64 = 30;
+
+    /// #############
+    /// Basic Queries
+    pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
+        db_run! { conn:
+            sqlite, mysql {
+                diesel::replace_into(event::table)
+                .values(EventDb::to_db(self))
+                .execute(conn)
+                .map_res("Error saving event")
+            }
+            postgresql {
+                diesel::insert_into(event::table)
+                .values(EventDb::to_db(self))
+                .on_conflict(event::uuid)
+                .do_update()
+                .set(EventDb::to_db(self))
+                .execute(conn)
+                .map_res("Error saving event")
+            }
+        }
+    }
+
+    pub async fn save_user_event(events: Vec<Event>, conn: &mut DbConn) -> EmptyResult {
+        // Special save function which is able to handle multiple events.
+        // SQLite doesn't support the DEFAULT argument, and does not support inserting multiple values at the same time.
+        // MySQL and PostgreSQL do.
+        // We also ignore duplicate if they ever will exists, else it could break the whole flow.
+        db_run! { conn:
+            // Unfortunately SQLite does not support inserting multiple records at the same time
+            // We loop through the events here and insert them one at a time.
+            sqlite {
+                for event in events {
+                    diesel::insert_or_ignore_into(event::table)
+                    .values(EventDb::to_db(&event))
+                    .execute(conn)
+                    .unwrap_or_default();
+                }
+                Ok(())
+            }
+            mysql {
+                let events: Vec<EventDb> = events.iter().map(EventDb::to_db).collect();
+                diesel::insert_or_ignore_into(event::table)
+                .values(&events)
+                .execute(conn)
+                .unwrap_or_default();
+                Ok(())
+            }
+            postgresql {
+                let events: Vec<EventDb> = events.iter().map(EventDb::to_db).collect();
+                diesel::insert_into(event::table)
+                .values(&events)
+                .on_conflict_do_nothing()
+                .execute(conn)
+                .unwrap_or_default();
+                Ok(())
+            }
+        }
+    }
+
+    pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
+        db_run! { conn: {
+            diesel::delete(event::table.filter(event::uuid.eq(self.uuid)))
+                .execute(conn)
+                .map_res("Error deleting event")
+        }}
+    }
+
+    /// ##############
+    /// Custom Queries
+    pub async fn find_by_organization_uuid(
+        org_uuid: &str,
+        start: &NaiveDateTime,
+        end: &NaiveDateTime,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
+        db_run! { conn: {
+            event::table
+                .filter(event::org_uuid.eq(org_uuid))
+                .filter(event::event_date.between(start, end))
+                .order_by(event::event_date.desc())
+                .limit(Self::PAGE_SIZE)
+                .load::<EventDb>(conn)
+                .expect("Error filtering events")
+                .from_db()
+        }}
+    }
+
+    pub async fn find_by_org_and_user_org(
+        org_uuid: &str,
+        user_org_uuid: &str,
+        start: &NaiveDateTime,
+        end: &NaiveDateTime,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
+        db_run! { conn: {
+            event::table
+                .inner_join(users_organizations::table.on(users_organizations::uuid.eq(user_org_uuid)))
+                .filter(event::org_uuid.eq(org_uuid))
+                .filter(event::event_date.between(start, end))
+                .filter(event::user_uuid.eq(users_organizations::user_uuid.nullable()).or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())))
+                .select(event::all_columns)
+                .order_by(event::event_date.desc())
+                .limit(Self::PAGE_SIZE)
+                .load::<EventDb>(conn)
+                .expect("Error filtering events")
+                .from_db()
+        }}
+    }
+
+    pub async fn find_by_cipher_uuid(
+        cipher_uuid: &str,
+        start: &NaiveDateTime,
+        end: &NaiveDateTime,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
+        db_run! { conn: {
+            event::table
+                .filter(event::cipher_uuid.eq(cipher_uuid))
+                .filter(event::event_date.between(start, end))
+                .order_by(event::event_date.desc())
+                .limit(Self::PAGE_SIZE)
+                .load::<EventDb>(conn)
+                .expect("Error filtering events")
+                .from_db()
+        }}
+    }
+
+    pub async fn clean_events(conn: &mut DbConn) -> EmptyResult {
+        if let Some(days_to_retain) = CONFIG.events_days_retain() {
+            let dt = Utc::now().naive_utc() - Duration::days(days_to_retain);
+            db_run! { conn: {
+                diesel::delete(event::table.filter(event::event_date.lt(dt)))
+                .execute(conn)
+                .map_res("Error cleaning old events")
+            }}
+        } else {
+            Ok(())
+        }
+    }
+}
diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs
index 20e659c6..274d48e8 100644
--- a/src/db/models/mod.rs
+++ b/src/db/models/mod.rs
@@ -3,6 +3,7 @@ mod cipher;
 mod collection;
 mod device;
 mod emergency_access;
+mod event;
 mod favorite;
 mod folder;
 mod group;
@@ -18,6 +19,7 @@ pub use self::cipher::Cipher;
 pub use self::collection::{Collection, CollectionCipher, CollectionUser};
 pub use self::device::Device;
 pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType};
+pub use self::event::{Event, EventType};
 pub use self::favorite::Favorite;
 pub use self::folder::{Folder, FolderCipher};
 pub use self::group::{CollectionGroup, Group, GroupUser};
diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs
index 0c4cadc4..3bc2ddad 100644
--- a/src/db/models/organization.rs
+++ b/src/db/models/organization.rs
@@ -3,6 +3,7 @@ use serde_json::Value;
 use std::cmp::Ordering;
 
 use super::{CollectionUser, GroupUser, OrgPolicy, OrgPolicyType, User};
+use crate::CONFIG;
 
 db_object! {
     #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -147,7 +148,7 @@ impl Organization {
             "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
             "Use2fa": true,
             "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
-            "UseEvents": false, // Not supported
+            "UseEvents": CONFIG.org_events_enabled(),
             "UseGroups": true,
             "UseTotp": true,
             "UsePolicies": true,
@@ -300,10 +301,9 @@ impl UserOrganization {
             "Seats": 10, // The value doesn't matter, we don't check server-side
             "MaxCollections": 10, // The value doesn't matter, we don't check server-side
             "UsersGetPremium": true,
-
             "Use2fa": true,
             "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
-            "UseEvents": false, // Not supported
+            "UseEvents": CONFIG.org_events_enabled(),
             "UseGroups": true,
             "UseTotp": true,
             // "UseScim": false, // Not supported (Not AGPLv3 Licensed)
@@ -629,6 +629,16 @@ impl UserOrganization {
         }}
     }
 
+    pub async fn get_org_uuid_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
+        db_run! { conn: {
+            users_organizations::table
+                .filter(users_organizations::user_uuid.eq(user_uuid))
+                .select(users_organizations::org_uuid)
+                .load::<String>(conn)
+                .unwrap_or_default()
+        }}
+    }
+
     pub async fn find_by_user_and_policy(user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
@@ -670,6 +680,18 @@ impl UserOrganization {
         }}
     }
 
+    pub async fn user_has_ge_admin_access_to_cipher(user_uuid: &str, cipher_uuid: &str, conn: &mut DbConn) -> bool {
+        db_run! { conn: {
+            users_organizations::table
+            .inner_join(ciphers::table.on(ciphers::uuid.eq(cipher_uuid).and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))))
+            .filter(users_organizations::user_uuid.eq(user_uuid))
+            .filter(users_organizations::atype.eq_any(vec![UserOrgType::Owner as i32, UserOrgType::Admin as i32]))
+            .count()
+            .first::<i64>(conn)
+            .ok().unwrap_or(0) != 0
+        }}
+    }
+
     pub async fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             users_organizations::table
diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs
index 514bc67a..0073a9d5 100644
--- a/src/db/schemas/mysql/schema.rs
+++ b/src/db/schemas/mysql/schema.rs
@@ -55,6 +55,27 @@ table! {
     }
 }
 
+table! {
+    event (uuid) {
+        uuid -> Varchar,
+        event_type -> Integer,
+        user_uuid -> Nullable<Varchar>,
+        org_uuid -> Nullable<Varchar>,
+        cipher_uuid -> Nullable<Varchar>,
+        collection_uuid -> Nullable<Varchar>,
+        group_uuid -> Nullable<Varchar>,
+        org_user_uuid -> Nullable<Varchar>,
+        act_user_uuid -> Nullable<Varchar>,
+        device_type -> Nullable<Integer>,
+        ip_address -> Nullable<Text>,
+        event_date -> Timestamp,
+        policy_uuid -> Nullable<Varchar>,
+        provider_uuid -> Nullable<Varchar>,
+        provider_user_uuid -> Nullable<Varchar>,
+        provider_org_uuid -> Nullable<Varchar>,
+    }
+}
+
 table! {
     favorites (user_uuid, cipher_uuid) {
         user_uuid -> Text,
@@ -272,6 +293,7 @@ joinable!(groups_users -> users_organizations (users_organizations_uuid));
 joinable!(groups_users -> groups (groups_uuid));
 joinable!(collections_groups -> collections (collections_uuid));
 joinable!(collections_groups -> groups (groups_uuid));
+joinable!(event -> users_organizations (uuid));
 
 allow_tables_to_appear_in_same_query!(
     attachments,
@@ -293,4 +315,5 @@ allow_tables_to_appear_in_same_query!(
     groups,
     groups_users,
     collections_groups,
+    event,
 );
diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs
index 23f9af7e..1421513c 100644
--- a/src/db/schemas/postgresql/schema.rs
+++ b/src/db/schemas/postgresql/schema.rs
@@ -55,6 +55,27 @@ table! {
     }
 }
 
+table! {
+    event (uuid) {
+        uuid -> Text,
+        event_type -> Integer,
+        user_uuid -> Nullable<Text>,
+        org_uuid -> Nullable<Text>,
+        cipher_uuid -> Nullable<Text>,
+        collection_uuid -> Nullable<Text>,
+        group_uuid -> Nullable<Text>,
+        org_user_uuid -> Nullable<Text>,
+        act_user_uuid -> Nullable<Text>,
+        device_type -> Nullable<Integer>,
+        ip_address -> Nullable<Text>,
+        event_date -> Timestamp,
+        policy_uuid -> Nullable<Text>,
+        provider_uuid -> Nullable<Text>,
+        provider_user_uuid -> Nullable<Text>,
+        provider_org_uuid -> Nullable<Text>,
+    }
+}
+
 table! {
     favorites (user_uuid, cipher_uuid) {
         user_uuid -> Text,
@@ -272,6 +293,7 @@ joinable!(groups_users -> users_organizations (users_organizations_uuid));
 joinable!(groups_users -> groups (groups_uuid));
 joinable!(collections_groups -> collections (collections_uuid));
 joinable!(collections_groups -> groups (groups_uuid));
+joinable!(event -> users_organizations (uuid));
 
 allow_tables_to_appear_in_same_query!(
     attachments,
@@ -293,4 +315,5 @@ allow_tables_to_appear_in_same_query!(
     groups,
     groups_users,
     collections_groups,
+    event,
 );
diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs
index 23f9af7e..0fedcf1d 100644
--- a/src/db/schemas/sqlite/schema.rs
+++ b/src/db/schemas/sqlite/schema.rs
@@ -55,6 +55,27 @@ table! {
     }
 }
 
+table! {
+    event (uuid) {
+        uuid -> Text,
+        event_type -> Integer,
+        user_uuid -> Nullable<Text>,
+        org_uuid -> Nullable<Text>,
+        cipher_uuid -> Nullable<Text>,
+        collection_uuid -> Nullable<Text>,
+        group_uuid -> Nullable<Text>,
+        org_user_uuid -> Nullable<Text>,
+        act_user_uuid -> Nullable<Text>,
+        device_type -> Nullable<Integer>,
+        ip_address -> Nullable<Text>,
+        event_date -> Timestamp,
+        policy_uuid -> Nullable<Text>,
+        provider_uuid -> Nullable<Text>,
+        provider_user_uuid -> Nullable<Text>,
+        provider_org_uuid -> Nullable<Text>,
+    }
+}
+
 table! {
     favorites (user_uuid, cipher_uuid) {
         user_uuid -> Text,
@@ -266,12 +287,14 @@ joinable!(users_collections -> collections (collection_uuid));
 joinable!(users_collections -> users (user_uuid));
 joinable!(users_organizations -> organizations (org_uuid));
 joinable!(users_organizations -> users (user_uuid));
+joinable!(users_organizations -> ciphers (org_uuid));
 joinable!(emergency_access -> users (grantor_uuid));
 joinable!(groups -> organizations (organizations_uuid));
 joinable!(groups_users -> users_organizations (users_organizations_uuid));
 joinable!(groups_users -> groups (groups_uuid));
 joinable!(collections_groups -> collections (collections_uuid));
 joinable!(collections_groups -> groups (groups_uuid));
+joinable!(event -> users_organizations (uuid));
 
 allow_tables_to_appear_in_same_query!(
     attachments,
@@ -293,4 +316,5 @@ allow_tables_to_appear_in_same_query!(
     groups,
     groups_users,
     collections_groups,
+    event,
 );
diff --git a/src/error.rs b/src/error.rs
index d42ecd20..decae01e 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,6 +1,7 @@
 //
 // Error generator macro
 //
+use crate::db::models::EventType;
 use std::error::Error as StdError;
 
 macro_rules! make_error {
@@ -8,14 +9,17 @@ macro_rules! make_error {
         const BAD_REQUEST: u16 = 400;
 
         pub enum ErrorKind { $($name( $ty )),+ }
-        pub struct Error { message: String, error: ErrorKind, error_code: u16 }
+
+        #[derive(Debug)]
+        pub struct ErrorEvent { pub event: EventType }
+        pub struct Error { message: String, error: ErrorKind, error_code: u16, event: Option<ErrorEvent> }
 
         $(impl From<$ty> for Error {
             fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }
         })+
         $(impl<S: Into<String>> From<(S, $ty)> for Error {
             fn from(val: (S, $ty)) -> Self {
-                Error { message: val.0.into(), error: ErrorKind::$name(val.1), error_code: BAD_REQUEST }
+                Error { message: val.0.into(), error: ErrorKind::$name(val.1), error_code: BAD_REQUEST, event: None }
             }
         })+
         impl StdError for Error {
@@ -130,6 +134,16 @@ impl Error {
         self.error_code = code;
         self
     }
+
+    #[must_use]
+    pub fn with_event(mut self, event: ErrorEvent) -> Self {
+        self.event = Some(event);
+        self
+    }
+
+    pub fn get_event(&self) -> &Option<ErrorEvent> {
+        &self.event
+    }
 }
 
 pub trait MapResult<S> {
@@ -216,12 +230,21 @@ macro_rules! err {
         error!("{}", $msg);
         return Err($crate::error::Error::new($msg, $msg));
     }};
+    ($msg:expr, ErrorEvent $err_event:tt) => {{
+        error!("{}", $msg);
+        return Err($crate::error::Error::new($msg, $msg).with_event($crate::error::ErrorEvent $err_event));
+    }};
     ($usr_msg:expr, $log_value:expr) => {{
         error!("{}. {}", $usr_msg, $log_value);
         return Err($crate::error::Error::new($usr_msg, $log_value));
     }};
+    ($usr_msg:expr, $log_value:expr, ErrorEvent $err_event:tt) => {{
+        error!("{}. {}", $usr_msg, $log_value);
+        return Err($crate::error::Error::new($usr_msg, $log_value).with_event($crate::error::ErrorEvent $err_event));
+    }};
 }
 
+#[macro_export]
 macro_rules! err_silent {
     ($msg:expr) => {{
         return Err($crate::error::Error::new($msg, $msg));
@@ -233,11 +256,11 @@ macro_rules! err_silent {
 
 #[macro_export]
 macro_rules! err_code {
-    ($msg:expr, $err_code: expr) => {{
+    ($msg:expr, $err_code:expr) => {{
         error!("{}", $msg);
         return Err($crate::error::Error::new($msg, $msg).with_code($err_code));
     }};
-    ($usr_msg:expr, $log_value:expr, $err_code: expr) => {{
+    ($usr_msg:expr, $log_value:expr, $err_code:expr) => {{
         error!("{}. {}", $usr_msg, $log_value);
         return Err($crate::error::Error::new($usr_msg, $log_value).with_code($err_code));
     }};
@@ -260,6 +283,9 @@ macro_rules! err_json {
     ($expr:expr, $log_value:expr) => {{
         return Err(($log_value, $expr).into());
     }};
+    ($expr:expr, $log_value:expr, $err_event:expr, ErrorEvent) => {{
+        return Err(($log_value, $expr).into().with_event($err_event));
+    }};
 }
 
 #[macro_export]
diff --git a/src/main.rs b/src/main.rs
index 83b3b64d..70cd5d9f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -430,6 +430,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
         .mount([basepath, "/"].concat(), api::web_routes())
         .mount([basepath, "/api"].concat(), api::core_routes())
         .mount([basepath, "/admin"].concat(), api::admin_routes())
+        .mount([basepath, "/events"].concat(), api::core_events_routes())
         .mount([basepath, "/identity"].concat(), api::identity_routes())
         .mount([basepath, "/icons"].concat(), api::icons_routes())
         .mount([basepath, "/notifications"].concat(), api::notifications_routes())
@@ -511,6 +512,16 @@ async fn schedule_jobs(pool: db::DbPool) {
                 }));
             }
 
+            // Cleanup the event table of records x days old.
+            if CONFIG.org_events_enabled()
+                && !CONFIG.event_cleanup_schedule().is_empty()
+                && CONFIG.events_days_retain().is_some()
+            {
+                sched.add(Job::new(CONFIG.event_cleanup_schedule().parse().unwrap(), || {
+                    runtime.spawn(api::event_cleanup_job(pool.clone()));
+                }));
+            }
+
             // Periodically check for jobs to run. We probably won't need any
             // jobs that run more often than once a minute, so a default poll
             // interval of 30 seconds should be sufficient. Users who want to
diff --git a/src/util.rs b/src/util.rs
index 41de7304..c3dde2bb 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -456,10 +456,13 @@ pub fn get_env_bool(key: &str) -> Option<bool> {
 
 use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
 
+// Format used by Bitwarden API
+const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ";
+
 /// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API
 /// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.).
 pub fn format_date(dt: &NaiveDateTime) -> String {
-    dt.format("%Y-%m-%dT%H:%M:%S%.6fZ").to_string()
+    dt.format(DATETIME_FORMAT).to_string()
 }
 
 /// Formats a `DateTime<Local>` using the specified format string.
@@ -500,6 +503,10 @@ pub fn format_datetime_http(dt: &DateTime<Local>) -> String {
     expiry_time.to_rfc2822().replace("+0000", "GMT")
 }
 
+pub fn parse_date(date: &str) -> NaiveDateTime {
+    NaiveDateTime::parse_from_str(date, DATETIME_FORMAT).unwrap()
+}
+
 //
 // Deployment environment methods
 //

From 7d506f3633c4acc576a5e752b342b6b5bd120c37 Mon Sep 17 00:00:00 2001
From: BlackDex <black.dex@gmail.com>
Date: Thu, 1 Dec 2022 11:45:26 +0100
Subject: [PATCH 4/4] Update Vaultwarden Logo's

Updated the logo's so the `V` is better visible.
Also the cog it self is better now, the previous version wasn't fully round.
These versions also are used with the PR to update the web-vault and use these logo's.

Also updated the images in the static folder.
---
 resources/vaultwarden-icon-white.svg   | 147 +++++-----
 resources/vaultwarden-icon.svg         | 134 +++++-----
 resources/vaultwarden-logo-white.svg   | 355 ++++++-------------------
 resources/vaultwarden-logo.svg         | 235 ++++++----------
 src/static/images/logo-gray.png        | Bin 2569 -> 2406 bytes
 src/static/images/vaultwarden-icon.png | Bin 945 -> 1459 bytes
 6 files changed, 296 insertions(+), 575 deletions(-)

diff --git a/resources/vaultwarden-icon-white.svg b/resources/vaultwarden-icon-white.svg
index e0aebef1..bb241d51 100644
--- a/resources/vaultwarden-icon-white.svg
+++ b/resources/vaultwarden-icon-white.svg
@@ -1,91 +1,70 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   width="2500"
-   height="2500"
-   preserveAspectRatio="xMidYMid"
-   version="1.1"
-   viewBox="0 0 256 256"
-   id="svg17"
-   sodipodi:docname="vaultwarden-icon-white.svg"
-   inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
-   inkscape:export-filename="vaultwarden-icon-white.png"
-   inkscape:export-xdpi="13"
-   inkscape:export-ydpi="13"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:dc="http://purl.org/dc/elements/1.1/">
-  <title
-     id="title33">Vaultwarden Icon - White</title>
-  <sodipodi:namedview
-     id="namedview19"
-     pagecolor="#000000"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageshadow="2"
-     inkscape:pageopacity="0"
-     inkscape:pagecheckerboard="0"
-     showgrid="false"
-     inkscape:zoom="0.1684"
-     inkscape:cx="2960.2138"
-     inkscape:cy="1172.8028"
-     inkscape:window-width="1615"
-     inkscape:window-height="945"
-     inkscape:window-x="2192"
-     inkscape:window-y="59"
-     inkscape:window-maximized="0"
-     inkscape:current-layer="svg17" />
-  <defs
-     id="defs7">
-    <filter
-       id="a"
-       color-interpolation-filters="sRGB"
-       x="0"
-       y="0"
-       width="1"
-       height="1">
-      <feColorMatrix
-         result="color1"
-         type="hueRotate"
-         values="180"
-         id="feColorMatrix2" />
-      <feColorMatrix
-         result="color2"
-         values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 -0.21 -0.72 -0.07 2 0 "
-         id="feColorMatrix4" />
-    </filter>
-  </defs>
-  <g
-     filter="url(#a)"
-     id="g15">
-    <path
-       d="m254.25 124.86-10.747-6.653c-0.0907-1.0444-0.19275-2.0878-0.306-3.13l9.236-8.615c0.93919-0.87247 1.3576-2.1702 1.105-3.427-0.24667-1.2576-1.129-2.2967-2.33-2.744l-11.807-4.415c-0.29654-1.0195-0.6049-2.0356-0.925-3.048l7.365-10.229c1.609-2.2301 0.30752-5.3738-2.407-5.814l-12.45-2.025c-0.484-0.944-0.988-1.874-1.496-2.796l5.231-11.483c0.53555-1.1652 0.42645-2.5251-0.288-3.59-0.70881-1.0684-1.9228-1.6905-3.204-1.642l-12.636 0.44c-0.65261-0.8174-1.318-1.6245-1.996-2.421l2.904-12.308c0.29536-1.2482-0.0779-2.5602-0.986-3.466-0.90579-0.90673-2.2165-1.2798-3.464-0.986l-12.305 2.901c-0.79878-0.67726-1.6075-1.3427-2.426-1.996l0.442-12.635c0.0481-1.2809-0.57299-2.4947-1.64-3.205-1.0652-0.71321-2.4241-0.82298-3.59-0.29l-11.48 5.234c-0.92601-0.51108-1.8581-1.0111-2.796-1.5l-2.03-12.452c-0.44306-2.7111-3.5818-4.011-5.812-2.407l-10.236 7.365c-1.007-0.32-2.02-0.629-3.042-0.922l-4.415-11.809c-0.44658-1.203-1.4861-2.0876-2.745-2.336-1.2557-0.24711-2.5501 0.171-3.424 1.106l-8.615 9.243c-1.0418-0.11676-2.0853-0.21877-3.13-0.306l-6.653-10.75c-1.4456-2.3359-4.8434-2.3359-6.289 0l-6.653 10.75c-1.0457 0.08711-2.0902 0.18912-3.133 0.306l-8.617-9.243c-1.8733-2.0136-5.2112-1.3481-6.169 1.23l-4.414 11.809c-1.023 0.293-2.035 0.604-3.045 0.922l-10.234-7.365c-1.0396-0.75151-2.395-0.90868-3.579-0.415-1.1826 0.49121-2.0278 1.5583-2.235 2.822l-2.03 12.452c-0.94 0.487-1.869 0.988-2.796 1.5l-11.481-5.235c-1.1653-0.53332-2.5239-0.42312-3.588 0.291-1.0678 0.7098-1.6897 1.9237-1.642 3.205l0.44 12.635c-0.81749 0.65454-1.6262 1.3199-2.426 1.996l-12.305-2.9c-1.2479-0.29242-2.5584 0.08-3.466 0.985-0.90811 0.9058-1.2814 2.2178-0.986 3.466l2.899 12.308c-0.673 0.797-1.338 1.604-1.991 2.421l-12.636-0.44c-1.2791-0.04037-2.4893 0.57942-3.204 1.641-0.71297 1.0652-0.82309 2.4238-0.291 3.59l5.234 11.484c-0.509 0.922-1.012 1.852-1.5 2.796l-12.449 2.025c-2.7128 0.44235-4.0133 3.5836-2.407 5.814l7.365 10.23c-0.32 1.01-0.631 2.024-0.925 3.047l-11.808 4.415c-2.5709 0.96582-3.2321 4.2964-1.225 6.171l9.237 8.614c-0.115 1.04-0.217 2.087-0.305 3.131l-10.749 6.653c-1.089 0.67419-1.7512 1.8642-1.75 3.145 0 1.284 0.663 2.473 1.751 3.143l10.748 6.653c0.088 1.047 0.19 2.092 0.305 3.131l-9.238 8.617c-2.0113 1.8728-1.3486 5.2075 1.226 6.169l11.808 4.415c0.294 1.022 0.605 2.037 0.925 3.047l-7.365 10.231c-1.6097 2.2305-0.30582 5.3751 2.41 5.812l12.447 2.025c0.487 0.944 0.986 1.874 1.5 2.8l-5.235 11.48c-0.53308 1.166-0.42291 2.5251 0.291 3.59 0.70985 1.0671 1.9233 1.6886 3.204 1.641l12.63-0.442c0.659 0.821 1.322 1.626 1.997 2.426l-2.899 12.31c-0.29516 1.246 0.07822 2.5559 0.986 3.459 0.90529 0.90894 2.2183 1.2813 3.466 0.983l12.305-2.898c0.8 0.68 1.61 1.34 2.427 1.99l-0.44 12.639c-0.09906 2.7469 2.7297 4.636 5.229 3.492l11.481-5.231c0.92443 0.51365 1.8565 1.0134 2.796 1.499l2.03 12.445c0.20391 1.2659 1.05 2.3353 2.235 2.825 1.1842 0.49049 2.5377 0.3343 3.579-0.413l10.229-7.37c1.01 0.32 2.025 0.633 3.047 0.927l4.415 11.804c0.44699 1.2014 1.4862 2.0841 2.744 2.331 1.2564 0.25347 2.5541-0.16559 3.425-1.106l8.617-9.238c1.04 0.12 2.086 0.22 3.133 0.313l6.653 10.748c0.67477 1.0874 1.8633 1.7491 3.143 1.75 1.2804-3.8e-4 2.4697-0.66218 3.145-1.75l6.653-10.748c1.047-0.093 2.092-0.193 3.131-0.313l8.615 9.238c0.871 0.93955 2.168 1.3585 3.424 1.106 1.2574-0.24777 2.2962-1.1302 2.744-2.331l4.415-11.804c1.022-0.294 2.038-0.607 3.048-0.927l10.231 7.37c2.2318 1.6044 5.372 0.30125 5.812-2.412l2.03-12.445c0.939-0.487 1.868-0.993 2.795-1.5l11.481 5.232c2.4998 1.1478 5.3319-0.74313 5.23-3.492l-0.44-12.638c0.81815-0.6508 1.6259-1.3146 2.423-1.991l12.306 2.898c1.25 0.294 2.56-0.07 3.463-0.983 0.90778-0.90311 1.2812-2.213 0.986-3.459l-2.898-12.31c0.675-0.8 1.34-1.605 1.99-2.426l12.636 0.442c1.2807 0.0484 2.4943-0.57285 3.204-1.64 0.71513-1.0654 0.82461-2.426 0.289-3.592l-5.232-11.478c0.511-0.927 1.013-1.857 1.497-2.8l12.45-2.026c1.2658-0.20323 2.3347-1.0502 2.822-2.236 0.49077-1.1834 0.33374-2.5365-0.415-3.576l-7.365-10.23c0.318-1.011 0.629-2.026 0.925-3.048l11.806-4.415c1.2018-0.44701 2.0846-1.4867 2.331-2.745 0.2531-1.2561-0.16593-2.5533-1.106-3.424l-9.235-8.617c0.112-1.04 0.215-2.086 0.305-3.13l10.748-6.654c1.0895-0.67288 1.7522-1.8625 1.751-3.143 0-1.281-0.66-2.472-1.749-3.145zm-71.932 89.156c-4.104-0.885-6.714-4.93-5.833-9.047 0.878-4.112 4.92-6.729 9.023-5.844 4.104 0.879 6.718 4.931 5.838 9.04-0.88 4.11-4.926 6.73-9.028 5.851zm-131-104.17c3.5006-1.5572 5.0777-5.6559 3.524-9.158 0 0-7.2499-16.112-7.0322-17.824 1.5218-11.97 10.589-23.092 20.547-31.809 10.245-8.9682 29.051-12.642 41.033-15.888 0.84451-0.22875 12.957 12.302 12.957 12.302 2.6416 2.7726 7.0324 2.8739 9.799 0.226l13.098-12.528c27.445 5.11 50.682 22.194 64.073 45.633l-8.967 20.253c-1.548 3.505 0.032 7.604 3.527 9.157l17.264 7.668c0.298 3.065 0.455 6.161 0.455 9.3 2.4014 22.942-8.1651 47.083-26.846 65.594l-16.082-3.456h-1e-3c-3.7456-0.80326-7.4322 1.5852-8.23 5.332l-3.816 17.807c-11.775 5.344-24.85 8.313-38.621 8.313-14.086 0-27.446-3.116-39.43-8.688l-3.814-17.806c-0.802-3.747-4.486-6.134-8.228-5.33l-15.72 3.376c-15.659-17.659-25.993-45.09-26.401-65.142 0-3.398 0.183-6.756 0.535-10.056v1e-3l16.376-7.277m21.559 103.8c-4.105 0.886-8.146-1.731-9.029-5.843-0.878-4.119 1.732-8.162 5.836-9.047 4.105-0.878 8.148 1.739 9.028 5.85 0.878 4.11-1.734 8.16-5.836 9.04zm-29.017-117.66c1.703 3.842-0.03 8.345-3.867 10.045-3.837 1.705-8.328-0.03-10.03-3.875-1.703-3.845 0.029-8.34 3.867-10.045 3.8394-1.7029 8.3321 0.03238 10.03 3.874zm78.492-71.241c3.033-2.905 7.844-2.79 10.748 0.247 2.898 3.046 2.788 7.862-0.252 10.765-3.033 2.906-7.844 2.793-10.748-0.25-2.8972-3.0434-2.7845-7.8576 0.252-10.762zm88.983 71.61c1.697-3.8415 6.1897-5.5762 10.028-3.872 3.838 1.702 5.57 6.203 3.867 10.045-1.6963 3.8433-6.1904 5.5796-10.03 3.875-3.833-1.703-5.565-6.2-3.865-10.048z"
-       color="#000000"
-       color-rendering="auto"
-       dominant-baseline="auto"
-       image-rendering="auto"
-       shape-rendering="auto"
-       solid-color="#000000"
-       stop-color="#000000"
-       style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-east-asian:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;font-variation-settings:normal;inline-size:0;isolation:auto;mix-blend-mode:normal;shape-margin:0;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"
-       id="path9" />
-    <g
-       aria-label="V"
-       id="g13">
-      <path
-         d="m40.368 56.262 35.459-17.039 51.802 139.85 47.083-133.45 40.067 10.639-61.629 166.39h-51.153z"
-         stroke-width=".21397"
-         id="path11" />
+<svg version="1.1" viewBox="0 0 256 256" id="svg3917" sodipodi:docname="vaultwarden-icon-white.svg" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" width="256" height="256" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs id="defs3921" />
+  <sodipodi:namedview id="namedview3919" pagecolor="#000000" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#202020" showgrid="false" inkscape:zoom="3.3359375" inkscape:cx="128" inkscape:cy="128" inkscape:window-width="1874" inkscape:window-height="1056" inkscape:window-x="46" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg3917" />
+  <title id="title3817">Vaultwarden Icon - White</title>
+  <g id="logo" transform="matrix(2.4381018,0,0,2.4381018,128,128)">
+    <g id="gear" mask="url(#holes)" stroke="#fff">
+      <path d="m-31.1718-33.813208 26.496029 74.188883h9.3515399l26.49603-74.188883h-9.767164l-16.728866 47.588948q-1.662496 4.571864-2.805462 8.624198-1.142966 3.948427-1.870308 7.585137-.72734199-3.63671-1.8703079-7.689043-1.142966-4.052334-2.805462-8.728104l-16.624959-47.381136z" fill="#fff" stroke-width="4.51171" id="path3819" />
+      <circle transform="scale(-1,1)" r="43" fill="none" stroke-width="9" id="circle3821" />
+      <g id="cogs" transform="scale(-1,1)">
+        <polygon id="cog" points="51 0 46 -3 46 3" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="3" />
+        <g fill="#fff" stroke="#fff" id="g3886">
+          <use transform="rotate(11.25)" xlink:href="#cog" id="use3824" />
+          <use transform="rotate(22.5)" xlink:href="#cog" id="use3826" />
+          <use transform="rotate(33.75)" xlink:href="#cog" id="use3828" />
+          <use transform="rotate(45)" xlink:href="#cog" id="use3830" />
+          <use transform="rotate(56.25)" xlink:href="#cog" id="use3832" />
+          <use transform="rotate(67.5)" xlink:href="#cog" id="use3834" />
+          <use transform="rotate(78.75)" xlink:href="#cog" id="use3836" />
+          <use transform="rotate(90)" xlink:href="#cog" id="use3838" />
+          <use transform="rotate(101.25)" xlink:href="#cog" id="use3840" />
+          <use transform="rotate(112.5)" xlink:href="#cog" id="use3842" />
+          <use transform="rotate(123.75)" xlink:href="#cog" id="use3844" />
+          <use transform="rotate(135)" xlink:href="#cog" id="use3846" />
+          <use transform="rotate(146.25)" xlink:href="#cog" id="use3848" />
+          <use transform="rotate(157.5)" xlink:href="#cog" id="use3850" />
+          <use transform="rotate(168.75)" xlink:href="#cog" id="use3852" />
+          <use transform="scale(-1)" xlink:href="#cog" id="use3854" />
+          <use transform="rotate(191.25)" xlink:href="#cog" id="use3856" />
+          <use transform="rotate(202.5)" xlink:href="#cog" id="use3858" />
+          <use transform="rotate(213.75)" xlink:href="#cog" id="use3860" />
+          <use transform="rotate(225)" xlink:href="#cog" id="use3862" />
+          <use transform="rotate(236.25)" xlink:href="#cog" id="use3864" />
+          <use transform="rotate(247.5)" xlink:href="#cog" id="use3866" />
+          <use transform="rotate(258.75)" xlink:href="#cog" id="use3868" />
+          <use transform="rotate(-90)" xlink:href="#cog" id="use3870" />
+          <use transform="rotate(-78.75)" xlink:href="#cog" id="use3872" />
+          <use transform="rotate(-67.5)" xlink:href="#cog" id="use3874" />
+          <use transform="rotate(-56.25)" xlink:href="#cog" id="use3876" />
+          <use transform="rotate(-45)" xlink:href="#cog" id="use3878" />
+          <use transform="rotate(-33.75)" xlink:href="#cog" id="use3880" />
+          <use transform="rotate(-22.5)" xlink:href="#cog" id="use3882" />
+          <use transform="rotate(-11.25)" xlink:href="#cog" id="use3884" />
+        </g>
+      </g>
+      <g id="mounts" transform="scale(-1,1)">
+        <polygon id="mount" points="0 -35 7 -42 -7 -42" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="6" />
+        <g fill="#fff" stroke="#fff" id="g3898">
+          <use transform="rotate(72)" xlink:href="#mount" id="use3890" />
+          <use transform="rotate(144)" xlink:href="#mount" id="use3892" />
+          <use transform="rotate(216)" xlink:href="#mount" id="use3894" />
+          <use transform="rotate(-72)" xlink:href="#mount" id="use3896" />
+        </g>
+      </g>
     </g>
+    <mask id="holes">
+      <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect3902" />
+      <circle id="hole" cy="-40" r="3" />
+      <use transform="rotate(72)" xlink:href="#hole" id="use3905" />
+      <use transform="rotate(144)" xlink:href="#hole" id="use3907" />
+      <use transform="rotate(216)" xlink:href="#hole" id="use3909" />
+      <use transform="rotate(-72)" xlink:href="#hole" id="use3911" />
+    </mask>
   </g>
-  <metadata
-     id="metadata31">
+  <metadata id="metadata3915">
     <rdf:RDF>
-      <cc:Work
-         rdf:about="">
+      <cc:Work rdf:about="">
         <dc:title>Vaultwarden Icon - White</dc:title>
         <dc:creator>
           <cc:Agent>
diff --git a/resources/vaultwarden-icon.svg b/resources/vaultwarden-icon.svg
index 67d617e6..91abbd6a 100644
--- a/resources/vaultwarden-icon.svg
+++ b/resources/vaultwarden-icon.svg
@@ -1,33 +1,66 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   width="2500"
-   height="2500"
-   preserveAspectRatio="xMidYMid"
-   version="1.1"
-   viewBox="0 0 256 256"
-   id="svg10"
-   sodipodi:docname="vaultwarden-icon.svg"
-   inkscape:export-filename="vaultwarden-icon.png"
-   inkscape:export-xdpi="144"
-   inkscape:export-ydpi="144"
-   inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:dc="http://purl.org/dc/elements/1.1/">
-  <title
-     id="title10">Vaultwarden Icon</title>
-  <metadata
-     id="metadata16">
+<svg version="1.1" viewBox="0 0 256 256" id="svg383" sodipodi:docname="vaultwarden-icon.svg" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" width="256" height="256" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs id="defs387" />
+  <sodipodi:namedview id="namedview385" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:zoom="3.3359375" inkscape:cx="128" inkscape:cy="128" inkscape:window-width="1874" inkscape:window-height="1056" inkscape:window-x="46" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg383" />
+  <title id="title287">Vaultwarden Icon</title>
+  <g id="logo" transform="matrix(2.4381018,0,0,2.4381018,128,128)">
+    <g id="gear" mask="url(#holes)">
+      <path d="m-31.1718-33.813208 26.496029 74.188883h9.3515399l26.49603-74.188883h-9.767164l-16.728866 47.588948q-1.662496 4.571864-2.805462 8.624198-1.142966 3.948427-1.870308 7.585137-.72734199-3.63671-1.8703079-7.689043-1.142966-4.052334-2.805462-8.728104l-16.624959-47.381136z" stroke="#000" stroke-width="4.51171" id="path289" />
+      <circle transform="scale(-1,1)" r="43" fill="none" stroke="#000" stroke-width="9" id="circle291" />
+      <g id="cogs" transform="scale(-1,1)">
+        <polygon id="cog" points="51 0 46 -3 46 3" stroke="#000" stroke-linejoin="round" stroke-width="3" />
+        <use transform="rotate(11.25)" xlink:href="#cog" id="use294" />
+        <use transform="rotate(22.5)" xlink:href="#cog" id="use296" />
+        <use transform="rotate(33.75)" xlink:href="#cog" id="use298" />
+        <use transform="rotate(45)" xlink:href="#cog" id="use300" />
+        <use transform="rotate(56.25)" xlink:href="#cog" id="use302" />
+        <use transform="rotate(67.5)" xlink:href="#cog" id="use304" />
+        <use transform="rotate(78.75)" xlink:href="#cog" id="use306" />
+        <use transform="rotate(90)" xlink:href="#cog" id="use308" />
+        <use transform="rotate(101.25)" xlink:href="#cog" id="use310" />
+        <use transform="rotate(112.5)" xlink:href="#cog" id="use312" />
+        <use transform="rotate(123.75)" xlink:href="#cog" id="use314" />
+        <use transform="rotate(135)" xlink:href="#cog" id="use316" />
+        <use transform="rotate(146.25)" xlink:href="#cog" id="use318" />
+        <use transform="rotate(157.5)" xlink:href="#cog" id="use320" />
+        <use transform="rotate(168.75)" xlink:href="#cog" id="use322" />
+        <use transform="scale(-1)" xlink:href="#cog" id="use324" />
+        <use transform="rotate(191.25)" xlink:href="#cog" id="use326" />
+        <use transform="rotate(202.5)" xlink:href="#cog" id="use328" />
+        <use transform="rotate(213.75)" xlink:href="#cog" id="use330" />
+        <use transform="rotate(225)" xlink:href="#cog" id="use332" />
+        <use transform="rotate(236.25)" xlink:href="#cog" id="use334" />
+        <use transform="rotate(247.5)" xlink:href="#cog" id="use336" />
+        <use transform="rotate(258.75)" xlink:href="#cog" id="use338" />
+        <use transform="rotate(-90)" xlink:href="#cog" id="use340" />
+        <use transform="rotate(-78.75)" xlink:href="#cog" id="use342" />
+        <use transform="rotate(-67.5)" xlink:href="#cog" id="use344" />
+        <use transform="rotate(-56.25)" xlink:href="#cog" id="use346" />
+        <use transform="rotate(-45)" xlink:href="#cog" id="use348" />
+        <use transform="rotate(-33.75)" xlink:href="#cog" id="use350" />
+        <use transform="rotate(-22.5)" xlink:href="#cog" id="use352" />
+        <use transform="rotate(-11.25)" xlink:href="#cog" id="use354" />
+      </g>
+      <g id="mounts" transform="scale(-1,1)">
+        <polygon id="mount" points="0 -35 7 -42 -7 -42" stroke="#000" stroke-linejoin="round" stroke-width="6" />
+        <use transform="rotate(72)" xlink:href="#mount" id="use358" />
+        <use transform="rotate(144)" xlink:href="#mount" id="use360" />
+        <use transform="rotate(216)" xlink:href="#mount" id="use362" />
+        <use transform="rotate(-72)" xlink:href="#mount" id="use364" />
+      </g>
+    </g>
+    <mask id="holes">
+      <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect368" />
+      <circle id="hole" cy="-40" r="3" />
+      <use transform="rotate(72)" xlink:href="#hole" id="use371" />
+      <use transform="rotate(144)" xlink:href="#hole" id="use373" />
+      <use transform="rotate(216)" xlink:href="#hole" id="use375" />
+      <use transform="rotate(-72)" xlink:href="#hole" id="use377" />
+    </mask>
+  </g>
+  <metadata id="metadata381">
     <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      <cc:Work rdf:about="">
         <dc:title>Vaultwarden Icon</dc:title>
         <dc:creator>
           <cc:Agent>
@@ -38,49 +71,4 @@
       </cc:Work>
     </rdf:RDF>
   </metadata>
-  <defs
-     id="defs14" />
-  <sodipodi:namedview
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1920"
-     inkscape:window-height="1049"
-     id="namedview12"
-     showgrid="false"
-     inkscape:zoom="0.24098199"
-     inkscape:cx="-487.5883"
-     inkscape:cy="985.55083"
-     inkscape:window-x="1920"
-     inkscape:window-y="0"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="svg10"
-     inkscape:pagecheckerboard="0" />
-  <g
-     id="g8">
-    <path
-       d="m254.25 124.86-10.747-6.653c-0.0907-1.0444-0.19275-2.0878-0.306-3.13l9.236-8.615c0.93919-0.87247 1.3576-2.1702 1.105-3.427-0.24667-1.2576-1.129-2.2967-2.33-2.744l-11.807-4.415c-0.29654-1.0195-0.6049-2.0356-0.925-3.048l7.365-10.229c1.609-2.2301 0.30752-5.3738-2.407-5.814l-12.45-2.025c-0.484-0.944-0.988-1.874-1.496-2.796l5.231-11.483c0.53555-1.1652 0.42645-2.5251-0.288-3.59-0.70881-1.0684-1.9228-1.6905-3.204-1.642l-12.636 0.44c-0.65261-0.8174-1.318-1.6245-1.996-2.421l2.904-12.308c0.29536-1.2482-0.0779-2.5602-0.986-3.466-0.90579-0.90673-2.2165-1.2798-3.464-0.986l-12.305 2.901c-0.79878-0.67726-1.6075-1.3427-2.426-1.996l0.442-12.635c0.0481-1.2809-0.57299-2.4947-1.64-3.205-1.0652-0.71321-2.4241-0.82298-3.59-0.29l-11.48 5.234c-0.92601-0.51108-1.8581-1.0111-2.796-1.5l-2.03-12.452c-0.44306-2.7111-3.5818-4.011-5.812-2.407l-10.236 7.365c-1.007-0.32-2.02-0.629-3.042-0.922l-4.415-11.809c-0.44658-1.203-1.4861-2.0876-2.745-2.336-1.2557-0.24711-2.5501 0.171-3.424 1.106l-8.615 9.243c-1.0418-0.11676-2.0853-0.21877-3.13-0.306l-6.653-10.75c-1.4456-2.3359-4.8434-2.3359-6.289 0l-6.653 10.75c-1.0457 0.08711-2.0902 0.18912-3.133 0.306l-8.617-9.243c-1.8733-2.0136-5.2112-1.3481-6.169 1.23l-4.414 11.809c-1.023 0.293-2.035 0.604-3.045 0.922l-10.234-7.365c-1.0396-0.75151-2.395-0.90868-3.579-0.415-1.1826 0.49121-2.0278 1.5583-2.235 2.822l-2.03 12.452c-0.94 0.487-1.869 0.988-2.796 1.5l-11.481-5.235c-1.1653-0.53332-2.5239-0.42312-3.588 0.291-1.0678 0.7098-1.6897 1.9237-1.642 3.205l0.44 12.635c-0.81749 0.65454-1.6262 1.3199-2.426 1.996l-12.305-2.9c-1.2479-0.29242-2.5584 0.08-3.466 0.985-0.90811 0.9058-1.2814 2.2178-0.986 3.466l2.899 12.308c-0.673 0.797-1.338 1.604-1.991 2.421l-12.636-0.44c-1.2791-0.04037-2.4893 0.57942-3.204 1.641-0.71297 1.0652-0.82309 2.4238-0.291 3.59l5.234 11.484c-0.509 0.922-1.012 1.852-1.5 2.796l-12.449 2.025c-2.7128 0.44235-4.0133 3.5836-2.407 5.814l7.365 10.23c-0.32 1.01-0.631 2.024-0.925 3.047l-11.808 4.415c-2.5709 0.96582-3.2321 4.2964-1.225 6.171l9.237 8.614c-0.115 1.04-0.217 2.087-0.305 3.131l-10.749 6.653c-1.089 0.67419-1.7512 1.8642-1.75 3.145 0 1.284 0.663 2.473 1.751 3.143l10.748 6.653c0.088 1.047 0.19 2.092 0.305 3.131l-9.238 8.617c-2.0113 1.8728-1.3486 5.2075 1.226 6.169l11.808 4.415c0.294 1.022 0.605 2.037 0.925 3.047l-7.365 10.231c-1.6097 2.2305-0.30582 5.3751 2.41 5.812l12.447 2.025c0.487 0.944 0.986 1.874 1.5 2.8l-5.235 11.48c-0.53308 1.166-0.42291 2.5251 0.291 3.59 0.70985 1.0671 1.9233 1.6886 3.204 1.641l12.63-0.442c0.659 0.821 1.322 1.626 1.997 2.426l-2.899 12.31c-0.29516 1.246 0.07822 2.5559 0.986 3.459 0.90529 0.90894 2.2183 1.2813 3.466 0.983l12.305-2.898c0.8 0.68 1.61 1.34 2.427 1.99l-0.44 12.639c-0.09906 2.7469 2.7297 4.636 5.229 3.492l11.481-5.231c0.92443 0.51365 1.8565 1.0134 2.796 1.499l2.03 12.445c0.20391 1.2659 1.05 2.3353 2.235 2.825 1.1842 0.49049 2.5377 0.3343 3.579-0.413l10.229-7.37c1.01 0.32 2.025 0.633 3.047 0.927l4.415 11.804c0.44699 1.2014 1.4862 2.0841 2.744 2.331 1.2564 0.25347 2.5541-0.16559 3.425-1.106l8.617-9.238c1.04 0.12 2.086 0.22 3.133 0.313l6.653 10.748c0.67477 1.0874 1.8633 1.7491 3.143 1.75 1.2804-3.8e-4 2.4697-0.66218 3.145-1.75l6.653-10.748c1.047-0.093 2.092-0.193 3.131-0.313l8.615 9.238c0.871 0.93955 2.168 1.3585 3.424 1.106 1.2574-0.24777 2.2962-1.1302 2.744-2.331l4.415-11.804c1.022-0.294 2.038-0.607 3.048-0.927l10.231 7.37c2.2318 1.6044 5.372 0.30125 5.812-2.412l2.03-12.445c0.939-0.487 1.868-0.993 2.795-1.5l11.481 5.232c2.4998 1.1478 5.3319-0.74313 5.23-3.492l-0.44-12.638c0.81815-0.6508 1.6259-1.3146 2.423-1.991l12.306 2.898c1.25 0.294 2.56-0.07 3.463-0.983 0.90778-0.90311 1.2812-2.213 0.986-3.459l-2.898-12.31c0.675-0.8 1.34-1.605 1.99-2.426l12.636 0.442c1.2807 0.0484 2.4943-0.57285 3.204-1.64 0.71513-1.0654 0.82461-2.426 0.289-3.592l-5.232-11.478c0.511-0.927 1.013-1.857 1.497-2.8l12.45-2.026c1.2658-0.20323 2.3347-1.0502 2.822-2.236 0.49077-1.1834 0.33374-2.5365-0.415-3.576l-7.365-10.23c0.318-1.011 0.629-2.026 0.925-3.048l11.806-4.415c1.2018-0.44701 2.0846-1.4867 2.331-2.745 0.2531-1.2561-0.16593-2.5533-1.106-3.424l-9.235-8.617c0.112-1.04 0.215-2.086 0.305-3.13l10.748-6.654c1.0895-0.67288 1.7522-1.8625 1.751-3.143 0-1.281-0.66-2.472-1.749-3.145zm-71.932 89.156c-4.104-0.885-6.714-4.93-5.833-9.047 0.878-4.112 4.92-6.729 9.023-5.844 4.104 0.879 6.718 4.931 5.838 9.04-0.88 4.11-4.926 6.73-9.028 5.851zm-131-104.17c3.5006-1.5572 5.0777-5.6559 3.524-9.158 0 0-7.2499-16.112-7.0322-17.824 1.5218-11.97 10.589-23.092 20.547-31.809 10.245-8.9682 29.051-12.642 41.033-15.888 0.84451-0.22875 12.957 12.302 12.957 12.302 2.6416 2.7726 7.0324 2.8739 9.799 0.226l13.098-12.528c27.445 5.11 50.682 22.194 64.073 45.633l-8.967 20.253c-1.548 3.505 0.032 7.604 3.527 9.157l17.264 7.668c0.298 3.065 0.455 6.161 0.455 9.3 2.4014 22.942-8.1651 47.083-26.846 65.594l-16.082-3.456h-1e-3c-3.7456-0.80326-7.4322 1.5852-8.23 5.332l-3.816 17.807c-11.775 5.344-24.85 8.313-38.621 8.313-14.086 0-27.446-3.116-39.43-8.688l-3.814-17.806c-0.802-3.747-4.486-6.134-8.228-5.33l-15.72 3.376c-15.659-17.659-25.993-45.09-26.401-65.142 0-3.398 0.183-6.756 0.535-10.056v1e-3l16.376-7.277m21.559 103.8c-4.105 0.886-8.146-1.731-9.029-5.843-0.878-4.119 1.732-8.162 5.836-9.047 4.105-0.878 8.148 1.739 9.028 5.85 0.878 4.11-1.734 8.16-5.836 9.04zm-29.017-117.66c1.703 3.842-0.03 8.345-3.867 10.045-3.837 1.705-8.328-0.03-10.03-3.875-1.703-3.845 0.029-8.34 3.867-10.045 3.8394-1.7029 8.3321 0.03238 10.03 3.874zm78.492-71.241c3.033-2.905 7.844-2.79 10.748 0.247 2.898 3.046 2.788 7.862-0.252 10.765-3.033 2.906-7.844 2.793-10.748-0.25-2.8972-3.0434-2.7845-7.8576 0.252-10.762zm88.983 71.61c1.697-3.8415 6.1897-5.5762 10.028-3.872 3.838 1.702 5.57 6.203 3.867 10.045-1.6963 3.8433-6.1904 5.5796-10.03 3.875-3.833-1.703-5.565-6.2-3.865-10.048z"
-       color="#000000"
-       color-rendering="auto"
-       dominant-baseline="auto"
-       image-rendering="auto"
-       shape-rendering="auto"
-       solid-color="#000000"
-       stop-color="#000000"
-       style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-east-asian:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;font-variation-settings:normal;inline-size:0;isolation:auto;mix-blend-mode:normal;shape-margin:0;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"
-       id="path2" />
-    <g
-       aria-label="V"
-       id="g6">
-      <path
-         d="m40.368 56.262 35.459-17.039 51.802 139.85 47.083-133.45 40.067 10.639-61.629 166.39h-51.153z"
-         stroke-width=".21397"
-         id="path4" />
-    </g>
-  </g>
 </svg>
diff --git a/resources/vaultwarden-logo-white.svg b/resources/vaultwarden-logo-white.svg
index ade44f23..49a75eb6 100644
--- a/resources/vaultwarden-logo-white.svg
+++ b/resources/vaultwarden-logo-white.svg
@@ -1,271 +1,88 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   version="1.1"
-   id="Capa_1"
-   x="0px"
-   y="0px"
-   viewBox="0 0 116.05518 23.833332"
-   xml:space="preserve"
-   sodipodi:docname="vaultwarden-logo-white.svg"
-   inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
-   width="116.05518"
-   height="23.833332"
-   inkscape:export-filename="vaultwarden-logo-white.png"
-   inkscape:export-xdpi="144"
-   inkscape:export-ydpi="144"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:dc="http://purl.org/dc/elements/1.1/"><title
-   id="title57">Vaultwarden Logo - White</title><metadata
-   id="metadata45"><rdf:RDF><cc:Work
-       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title>Vaultwarden Logo - White</dc:title><dc:creator><cc:Agent><dc:title>Mathijs van Veluw</dc:title></cc:Agent></dc:creator><dc:relation>Rust Logo</dc:relation></cc:Work></rdf:RDF></metadata><defs
-   id="defs43"><filter
-     style="color-interpolation-filters:sRGB"
-     inkscape:label="Invert"
-     id="filter33"
-     x="0"
-     y="0"
-     width="1"
-     height="1"><feColorMatrix
-       type="hueRotate"
-       values="180"
-       result="color1"
-       id="feColorMatrix29" /><feColorMatrix
-       values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       id="feColorMatrix31" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix53" /><feColorMatrix
-       id="feColorMatrix55"
-       values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       in="fbSourceGraphic" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix61" /><feColorMatrix
-       id="feColorMatrix63"
-       values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       in="fbSourceGraphic" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix69" /><feColorMatrix
-       id="feColorMatrix71"
-       values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       in="fbSourceGraphic" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix77" /><feColorMatrix
-       id="feColorMatrix79"
-       values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       in="fbSourceGraphic" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix85" /><feColorMatrix
-       id="feColorMatrix87"
-       type="hueRotate"
-       values="180"
-       result="color1"
-       in="fbSourceGraphic" /><feColorMatrix
-       id="feColorMatrix89"
-       values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 -0.21 -0.72 -0.07 2 0 "
-       result="color2" /></filter><filter
-     style="color-interpolation-filters:sRGB"
-     inkscape:label="Invert"
-     id="filter39"
-     x="0"
-     y="0"
-     width="1"
-     height="1"><feColorMatrix
-       type="hueRotate"
-       values="180"
-       result="color1"
-       id="feColorMatrix35" /><feColorMatrix
-       values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       id="feColorMatrix37" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix57" /><feColorMatrix
-       id="feColorMatrix59"
-       values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       in="fbSourceGraphic" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix65" /><feColorMatrix
-       id="feColorMatrix67"
-       values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       in="fbSourceGraphic" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix73" /><feColorMatrix
-       id="feColorMatrix75"
-       values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       in="fbSourceGraphic" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix81" /><feColorMatrix
-       id="feColorMatrix83"
-       values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
-       result="fbSourceGraphic"
-       in="fbSourceGraphic" /><feColorMatrix
-       result="fbSourceGraphicAlpha"
-       in="fbSourceGraphic"
-       values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
-       id="feColorMatrix91" /><feColorMatrix
-       id="feColorMatrix93"
-       type="hueRotate"
-       values="180"
-       result="color1"
-       in="fbSourceGraphic" /><feColorMatrix
-       id="feColorMatrix95"
-       values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 -0.21 -0.72 -0.07 2 0 "
-       result="color2" /></filter></defs><sodipodi:namedview
-   pagecolor="#000000"
-   bordercolor="#666666"
-   borderopacity="1"
-   objecttolerance="10"
-   gridtolerance="10"
-   guidetolerance="10"
-   inkscape:pageopacity="0"
-   inkscape:pageshadow="2"
-   inkscape:window-width="1876"
-   inkscape:window-height="1050"
-   id="namedview41"
-   showgrid="false"
-   inkscape:zoom="4.0586988"
-   inkscape:cx="56.298832"
-   inkscape:cy="6.1596096"
-   inkscape:window-x="44"
-   inkscape:window-y="30"
-   inkscape:window-maximized="1"
-   inkscape:current-layer="g8"
-   inkscape:document-rotation="0"
-   fit-margin-top="0"
-   fit-margin-left="0"
-   fit-margin-right="0"
-   fit-margin-bottom="0"
-   showguides="true"
-   inkscape:pagecheckerboard="false" />
-<g
-   id="g8"
-   transform="translate(-78.706331,-66.544831)">
-
-
-
-<text
-   xml:space="preserve"
-   style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:200px;line-height:1.25;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';fill:#e6e6e6;fill-opacity:1;stroke:none;filter:url(#filter33)"
-   x="286.59244"
-   y="223.43649"
-   id="text129"
-   transform="matrix(0.08497052,0,0,0.08497047,77.796719,65.754881)"><tspan
-     sodipodi:role="line"
-     id="tspan127"
-     x="286.59244"
-     y="223.43649"
-     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:200px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';fill:#e6e6e6"><tspan
-   style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:200px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans Bold';fill:#e6e6e6"
-   id="tspan30">ault</tspan>warden</tspan></text><g
-   id="g49"
-   transform="matrix(0.14313631,0,0,0.14313623,119.81603,58.408913)"
-   style="fill:#e6e6e6;filter:url(#filter39)"><path
-     d="m -121.83937,138.05241 -6.98995,-4.32717 c -0.059,-0.67929 -0.12536,-1.35792 -0.19902,-2.03579 l 6.00718,-5.60327 c 0.61086,-0.56747 0.88299,-1.41152 0.7187,-2.22896 -0.16043,-0.81796 -0.73431,-1.49379 -1.51545,-1.78472 l -7.67938,-2.87156 c -0.19288,-0.66309 -0.39344,-1.32396 -0.60164,-1.98244 l 4.79027,-6.65304 c 1.04651,-1.45048 0.20002,-3.49518 -1.56553,-3.78148 l -8.0976,-1.31708 c -0.3148,-0.61399 -0.64261,-1.21887 -0.97302,-1.81855 l 3.4023,-7.468656 c 0.34833,-0.757849 0.27736,-1.642349 -0.18731,-2.334963 -0.46103,-0.694895 -1.25061,-1.099512 -2.08393,-1.067976 l -8.21857,0.286187 c -0.42446,-0.531648 -0.85724,-1.056598 -1.29822,-1.574649 l 1.88879,-8.005242 c 0.19212,-0.81184 -0.0507,-1.665186 -0.6413,-2.254309 -0.58913,-0.589746 -1.44163,-0.832402 -2.25301,-0.641314 l -8.0033,1.886847 c -0.51953,-0.4405 -1.04553,-0.873315 -1.57789,-1.298218 l 0.28748,-8.217927 c 0.0312,-0.833106 -0.37268,-1.622579 -1.06668,-2.084574 -0.69281,-0.463867 -1.57665,-0.53527 -2.33496,-0.188609 l -7.46671,3.40425 c -0.60228,-0.332412 -1.20852,-0.657629 -1.81853,-0.975633 l -1.32034,-8.09889 c -0.28816,-1.76333 -2.32963,-2.608798 -3.78018,-1.565537 l -6.6576,4.790259 c -0.65496,-0.208119 -1.31382,-0.409101 -1.97854,-0.599674 l -2.87156,-7.680673 c -0.29045,-0.782461 -0.96657,-1.357799 -1.78537,-1.519368 -0.81672,-0.160726 -1.65861,0.111226 -2.22701,0.719365 l -5.60328,6.011731 c -0.67759,-0.07595 -1.35629,-0.142297 -2.03578,-0.199033 l -4.32716,-6.9919 c -0.94024,-1.519281 -3.1502,-1.519281 -4.09044,0 l -4.32716,6.9919 c -0.68014,0.05667 -1.3595,0.123017 -2.03774,0.199033 l -5.60459,-6.011731 c -1.2184,-1.309662 -3.3894,-0.87683 -4.01237,0.800003 l -2.8709,7.680673 c -0.66538,0.190573 -1.32359,0.392845 -1.9805,0.599674 l -6.6563,-4.790259 c -0.67616,-0.488791 -1.55773,-0.591006 -2.32781,-0.269916 -0.76917,0.319483 -1.3189,1.013531 -1.45366,1.835453 l -1.32034,8.09889 c -0.61138,0.316759 -1.21562,0.642619 -1.81854,0.975633 l -7.46735,-3.404903 c -0.75792,-0.346886 -1.64156,-0.275192 -2.33367,0.189262 -0.69451,0.461668 -1.099,1.251207 -1.06798,2.084574 l 0.28618,8.217927 c -0.53169,0.425714 -1.0577,0.858479 -1.5779,1.298218 l -8.00328,-1.886194 c -0.81164,-0.19019 -1.66401,0.05203 -2.25433,0.640661 -0.59063,0.589123 -0.83342,1.442469 -0.6413,2.254309 l 1.88555,8.005242 c -0.43773,0.518378 -0.87025,1.043255 -1.29497,1.574649 l -8.21858,-0.286187 c -0.83193,-0.02623 -1.61905,0.376856 -2.0839,1.067317 -0.46373,0.692824 -0.53536,1.576466 -0.18927,2.334984 l 3.40423,7.469294 c -0.33105,0.59968 -0.6582,1.20456 -0.97561,1.81855 l -8.09694,1.31708 c -1.76444,0.2877 -2.6103,2.33081 -1.56554,3.78148 l 4.79027,6.65368 c -0.20813,0.65693 -0.41041,1.31643 -0.60163,1.9818 l -7.68004,2.87156 c -1.67214,0.62818 -2.10219,2.7944 -0.79676,4.01368 l 6.00784,5.60261 c -0.0747,0.67644 -0.14113,1.35742 -0.19837,2.03645 l -6.99126,4.32717 c -0.70829,0.4385 -1.13899,1.2125 -1.13821,2.04553 0,0.83513 0.43122,1.60847 1.13887,2.04423 l 6.9906,4.32719 c 0.0573,0.68097 0.12357,1.36066 0.19837,2.03643 l -6.00848,5.60458 c -1.30817,1.21808 -0.87715,3.387 0.7974,4.01237 l 7.68004,2.87156 c 0.19122,0.66472 0.3935,1.32488 0.60163,1.98181 l -4.79027,6.65433 c -1.04696,1.45074 -0.19891,3.49602 1.56749,3.78018 l 8.09565,1.31708 c 0.31675,0.61398 0.6413,1.21886 0.97561,1.82114 l -3.40489,7.46671 c -0.34673,0.75837 -0.27507,1.64234 0.18927,2.33497 0.46169,0.69405 1.25093,1.09828 2.0839,1.06732 l 8.21468,-0.28748 c 0.42862,0.53398 0.85984,1.05756 1.29887,1.57789 l -1.88555,8.00654 c -0.19197,0.81042 0.0509,1.66238 0.6413,2.24977 0.58882,0.59119 1.44282,0.83337 2.25433,0.63935 l 8.00328,-1.88488 c 0.52034,0.44228 1.04717,0.87155 1.57856,1.29431 l -0.28619,8.22052 c -0.0643,1.78662 1.77543,3.01531 3.401,2.27124 l 7.46735,-3.4023 c 0.60126,0.33409 1.20748,0.65913 1.81854,0.97497 l 1.32034,8.09435 c 0.13262,0.82335 0.68292,1.51889 1.45366,1.83741 0.77022,0.31902 1.65055,0.21743 2.32781,-0.26862 l 6.65304,-4.79353 c 0.65692,0.20813 1.31708,0.41172 1.9818,0.60294 l 2.87156,7.67743 c 0.29073,0.78141 0.96664,1.35551 1.78472,1.5161 0.81718,0.16487 1.66121,-0.1077 2.22766,-0.71935 l 5.60458,-6.00848 c 0.67642,0.0781 1.35675,0.14309 2.03773,0.20357 l 4.32717,6.99061 c 0.43889,0.70725 1.21192,1.13762 2.04425,1.13821 0.83277,-3.4e-4 1.60631,-0.43068 2.04553,-1.13821 l 4.32717,-6.99061 c 0.68098,-0.0605 1.36066,-0.12552 2.03644,-0.20357 l 5.60328,6.00848 c 0.5665,0.6111 1.41008,0.88358 2.227,0.71935 0.81783,-0.16115 1.49347,-0.73509 1.78472,-1.5161 l 2.87156,-7.67743 c 0.66472,-0.19122 1.32553,-0.39481 1.98245,-0.60294 l 6.65434,4.79353 c 1.45159,1.04351 3.494,0.19593 3.78018,-1.56879 l 1.32032,-8.09435 c 0.61074,-0.31675 1.21498,-0.64586 1.81789,-0.97561 l 7.46737,3.40294 c 1.62589,0.74654 3.46791,-0.48334 3.40163,-2.27124 l -0.28617,-8.21987 c 0.53214,-0.42329 1.0575,-0.85503 1.57594,-1.29496 l 8.00394,1.88488 c 0.81302,0.19123 1.66505,-0.0455 2.25237,-0.63935 0.59042,-0.5874 0.83331,-1.43935 0.64131,-2.24977 l -1.8849,-8.00654 c 0.43903,-0.52033 0.87155,-1.04391 1.29432,-1.57789 l 8.21858,0.28748 c 0.83297,0.0315 1.62231,-0.37259 2.0839,-1.06667 0.46514,-0.69295 0.53634,-1.57789 0.18797,-2.33628 l -3.40294,-7.46539 c 0.33236,-0.60294 0.65887,-1.20782 0.97367,-1.82116 l 8.09759,-1.31772 c 0.82329,-0.13219 1.51852,-0.68305 1.83546,-1.45431 0.31921,-0.76971 0.21708,-1.64977 -0.26992,-2.32587 l -4.79026,-6.6537 c 0.20683,-0.65756 0.4091,-1.31772 0.60162,-1.98244 l 7.67874,-2.87156 c 0.78166,-0.29074 1.35584,-0.96697 1.5161,-1.78537 0.16462,-0.81698 -0.10792,-1.66069 -0.71935,-2.227 l -6.00653,-5.60458 c 0.0728,-0.67643 0.13983,-1.35675 0.19837,-2.03578 l 6.99061,-4.32784 c 0.70862,-0.43763 1.13964,-1.21137 1.13887,-2.04423 0,-0.83317 -0.42927,-1.60781 -1.13758,-2.04553 z m -46.78525,57.98791 c -2.66929,-0.57561 -4.36685,-3.20652 -3.79384,-5.88425 0.57106,-2.67449 3.20002,-4.37661 5.86864,-3.801 2.66929,0.57171 4.36945,3.20718 3.79709,5.87971 -0.57236,2.67317 -3.20391,4.37725 -5.87189,3.80554 z m -85.20366,-67.75316 c 2.27683,-1.01282 3.30259,-3.67866 2.29205,-5.95646 0,0 -4.7154,-10.47937 -4.57381,-11.59289 0.9898,-7.7854 6.88718,-15.019243 13.36396,-20.688869 6.66345,-5.833 18.89505,-8.222474 26.68826,-10.333706 0.54928,-0.148782 8.42735,8.001345 8.42735,8.001345 1.71813,1.803314 4.57395,1.869198 6.37337,0.146991 l 8.51906,-8.148336 c 17.85049,3.323592 32.96406,14.435192 41.67369,29.680135 l -5.83221,13.17273 c -1.00685,2.2797 0.0208,4.94572 2.29398,5.95581 l 11.22868,4.98735 c 0.19381,1.99351 0.29593,4.00716 0.29593,6.0488 1.5619,14.92169 -5.31066,30.62323 -17.46088,42.66296 l -10.4599,-2.24782 h -5.1e-4 c -2.43617,-0.52245 -4.83397,1.03104 -5.35287,3.46799 l -2.48197,11.58184 c -7.65856,3.47579 -16.16266,5.40686 -25.11946,5.40686 -9.16167,0 -17.85115,-2.02668 -25.64565,-5.65076 l -2.48064,-11.5812 c -0.52164,-2.43708 -2.91775,-3.98961 -5.35158,-3.46669 l -10.22443,2.19579 c -10.18477,-11.48557 -16.9061,-29.32695 -17.17146,-42.36897 0,-2.21009 0.11901,-4.39417 0.34796,-6.54053 v 0 l 10.65111,-4.73302 m 14.02218,67.51249 c -2.66994,0.57626 -5.29824,-1.12586 -5.87255,-3.80035 -0.57105,-2.67903 1.12651,-5.30864 3.79579,-5.88425 2.66994,-0.57106 5.29954,1.13106 5.8719,3.8049 0.57105,2.67318 -1.12781,5.30733 -3.7958,5.8797 z M -258.679,119.27181 c 1.10764,2.49887 -0.0195,5.42767 -2.51513,6.53337 -2.49564,1.10894 -5.41662,-0.0195 -6.52362,-2.52033 -1.10764,-2.50083 0.0189,-5.42442 2.51513,-6.53337 2.49719,-1.10758 5.41928,0.0211 6.52362,2.5197 z m 51.05194,-46.335823 c 1.9727,-1.889433 5.10182,-1.814638 6.9906,0.160653 1.88489,1.981149 1.81334,5.113514 -0.16389,7.001671 -1.9727,1.890086 -5.10182,1.816585 -6.99061,-0.162619 -1.88437,-1.979444 -1.81107,-5.110655 0.1639,-6.999705 z m 57.87539,46.575823 c 1.10375,-2.49856 4.02585,-3.6268 6.52231,-2.51838 2.49627,1.107 3.62279,4.03449 2.51514,6.53337 -1.1033,2.49972 -4.0263,3.62903 -6.52361,2.52033 -2.49301,-1.10765 -3.61953,-4.03253 -2.51384,-6.53532 z"
-     color="#000000"
-     color-rendering="auto"
-     dominant-baseline="auto"
-     image-rendering="auto"
-     shape-rendering="auto"
-     solid-color="#000000"
-     stop-color="#000000"
-     style="font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;isolation:auto;mix-blend-mode:normal;fill:#e6e6e6;stroke-width:0.65041"
-     id="path2-6" /><g
-     aria-label="V"
-     id="g6"
-     transform="matrix(0.65040957,0,0,0.65040957,-287.20599,56.842282)"
-     style="fill:#e6e6e6"><path
-       d="m 40.368,56.262 35.459,-17.039 51.802,139.85 47.083,-133.45 40.067,10.639 -61.629,166.39 h -51.153 z"
-       stroke-width="0.21397"
-       id="path4-7"
-       style="fill:#e6e6e6" /></g></g></g>
-<g
-   id="g10"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g12"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g14"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g16"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g18"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g20"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g22"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g24"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g26"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g28"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g30"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g32"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g34"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g36"
-   transform="translate(-78.706331,-66.544831)">
-</g>
-<g
-   id="g38"
-   transform="translate(-78.706331,-66.544831)">
-</g>
+<svg width="1365.8256" height="280.48944" version="1.1" viewBox="0 0 1365.8255 280.48944" id="svg3412" sodipodi:docname="vaultwarden-logo.svg" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <sodipodi:namedview id="namedview3414" pagecolor="#000000" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#202020" showgrid="false" inkscape:zoom="0.95107314" inkscape:cx="683.4385" inkscape:cy="139.84203" inkscape:window-width="1874" inkscape:window-height="1056" inkscape:window-x="46" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg3412" />
+  <title id="title3292">Vaultwarden Logo - White</title>
+  <defs id="defs3308">
+    <mask id="holes">
+      <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect3296" />
+      <circle id="hole" cy="-40" r="3" />
+      <use transform="rotate(72)" xlink:href="#hole" id="use3299" />
+      <use transform="rotate(144)" xlink:href="#hole" id="use3301" />
+      <use transform="rotate(216)" xlink:href="#hole" id="use3303" />
+      <use transform="rotate(-72)" xlink:href="#hole" id="use3305" />
+    </mask>
+  </defs>
+  <text transform="translate(-10.708266,-9.2965379)" x="286.59244" y="223.43649" fill="#e6e6e6" font-family="'Open Sans'" font-size="200px" style="line-height:1.25" xml:space="preserve" id="text3314"><tspan x="286.59244" y="223.43649" font-family="'Open Sans'" font-size="200px" id="tspan3312"><tspan font-family="'Open Sans'" font-size="200px" font-weight="bold" id="tspan3310">ault</tspan>warden</tspan></text>
+  <g transform="translate(-10.708266,-9.2965379)" id="g3410">
+    <g id="logo" transform="matrix(2.6712834,0,0,2.6712834,150.95027,149.53854)">
+      <g id="gear" mask="url(#holes)">
+        <path d="m-31.1718-33.813208 26.496029 74.188883h9.3515399l26.49603-74.188883h-9.767164l-16.728866 47.588948q-1.662496 4.571864-2.805462 8.624198-1.142966 3.948427-1.870308 7.585137-.72734199-3.63671-1.8703079-7.689043-1.142966-4.052334-2.805462-8.728104l-16.624959-47.381136z" fill="#e6e6e6" stroke="#e6e6e6" stroke-width="4.51171" id="path3316" />
+        <circle transform="scale(-1,1)" r="43" fill="none" stroke="#e6e6e6" stroke-width="9" id="circle3318" />
+        <g id="cogs" transform="scale(-1,1)">
+          <polygon id="cog" points="46 -3 46 3 51 0" fill="#e6e6e6" stroke="#e6e6e6" stroke-linejoin="round" stroke-width="3" />
+          <use transform="rotate(11.25)" xlink:href="#cog" id="use3321" />
+          <use transform="rotate(22.5)" xlink:href="#cog" id="use3323" />
+          <use transform="rotate(33.75)" xlink:href="#cog" id="use3325" />
+          <use transform="rotate(45)" xlink:href="#cog" id="use3327" />
+          <use transform="rotate(56.25)" xlink:href="#cog" id="use3329" />
+          <use transform="rotate(67.5)" xlink:href="#cog" id="use3331" />
+          <use transform="rotate(78.75)" xlink:href="#cog" id="use3333" />
+          <use transform="rotate(90)" xlink:href="#cog" id="use3335" />
+          <use transform="rotate(101.25)" xlink:href="#cog" id="use3337" />
+          <use transform="rotate(112.5)" xlink:href="#cog" id="use3339" />
+          <use transform="rotate(123.75)" xlink:href="#cog" id="use3341" />
+          <use transform="rotate(135)" xlink:href="#cog" id="use3343" />
+          <use transform="rotate(146.25)" xlink:href="#cog" id="use3345" />
+          <use transform="rotate(157.5)" xlink:href="#cog" id="use3347" />
+          <use transform="rotate(168.75)" xlink:href="#cog" id="use3349" />
+          <use transform="scale(-1)" xlink:href="#cog" id="use3351" />
+          <use transform="rotate(191.25)" xlink:href="#cog" id="use3353" />
+          <use transform="rotate(202.5)" xlink:href="#cog" id="use3355" />
+          <use transform="rotate(213.75)" xlink:href="#cog" id="use3357" />
+          <use transform="rotate(225)" xlink:href="#cog" id="use3359" />
+          <use transform="rotate(236.25)" xlink:href="#cog" id="use3361" />
+          <use transform="rotate(247.5)" xlink:href="#cog" id="use3363" />
+          <use transform="rotate(258.75)" xlink:href="#cog" id="use3365" />
+          <use transform="rotate(-90)" xlink:href="#cog" id="use3367" />
+          <use transform="rotate(-78.75)" xlink:href="#cog" id="use3369" />
+          <use transform="rotate(-67.5)" xlink:href="#cog" id="use3371" />
+          <use transform="rotate(-56.25)" xlink:href="#cog" id="use3373" />
+          <use transform="rotate(-45)" xlink:href="#cog" id="use3375" />
+          <use transform="rotate(-33.75)" xlink:href="#cog" id="use3377" />
+          <use transform="rotate(-22.5)" xlink:href="#cog" id="use3379" />
+          <use transform="rotate(-11.25)" xlink:href="#cog" id="use3381" />
+        </g>
+        <g id="mounts" transform="scale(-1,1)">
+          <polygon id="mount" points="7 -42 -7 -42 0 -35" stroke="#e6e6e6" stroke-linejoin="round" stroke-width="6" />
+          <use transform="rotate(72)" xlink:href="#mount" id="use3385" />
+          <use transform="rotate(144)" xlink:href="#mount" id="use3387" />
+          <use transform="rotate(216)" xlink:href="#mount" id="use3389" />
+          <use transform="rotate(-72)" xlink:href="#mount" id="use3391" />
+        </g>
+      </g>
+      <mask id="mask3407">
+        <rect x="-60" y="-60" width="120" height="120" fill="#e6e6e6" id="rect3395" />
+        <circle cy="-40" r="3" id="circle3397" />
+        <use transform="rotate(72)" xlink:href="#hole" id="use3399" />
+        <use transform="rotate(144)" xlink:href="#hole" id="use3401" />
+        <use transform="rotate(216)" xlink:href="#hole" id="use3403" />
+        <use transform="rotate(-72)" xlink:href="#hole" id="use3405" />
+      </mask>
+    </g>
+  </g>
+  <metadata id="metadata3294">
+    <rdf:RDF>
+      <cc:Work rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title>Vaultwarden Logo - White</dc:title>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Mathijs van Veluw</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:relation>Rust Logo</dc:relation>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
 </svg>
diff --git a/resources/vaultwarden-logo.svg b/resources/vaultwarden-logo.svg
index 330456cf..000cf2e9 100644
--- a/resources/vaultwarden-logo.svg
+++ b/resources/vaultwarden-logo.svg
@@ -1,151 +1,88 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   version="1.1"
-   id="Capa_1"
-   x="0px"
-   y="0px"
-   viewBox="0 0 1365.8255 280.48944"
-   xml:space="preserve"
-   sodipodi:docname="vaultwarden-logo.svg"
-   inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
-   width="1365.8256"
-   height="280.48944"
-   inkscape:export-filename="vaultwarden-logo.png"
-   inkscape:export-xdpi="144"
-   inkscape:export-ydpi="144"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:dc="http://purl.org/dc/elements/1.1/"><title
-   id="title29">Vaultwarden Logo</title><metadata
-   id="metadata45"><rdf:RDF><cc:Work
-       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title>Vaultwarden Logo</dc:title><dc:creator><cc:Agent><dc:title>Mathijs van Veluw</dc:title></cc:Agent></dc:creator><dc:relation>Rust Logo</dc:relation></cc:Work></rdf:RDF></metadata><defs
-   id="defs43" /><sodipodi:namedview
-   pagecolor="#ffffff"
-   bordercolor="#666666"
-   borderopacity="1"
-   objecttolerance="10"
-   gridtolerance="10"
-   guidetolerance="10"
-   inkscape:pageopacity="0"
-   inkscape:pageshadow="2"
-   inkscape:window-width="1920"
-   inkscape:window-height="1049"
-   id="namedview41"
-   showgrid="false"
-   inkscape:zoom="0.50733735"
-   inkscape:cx="99.539291"
-   inkscape:cy="136.98972"
-   inkscape:window-x="1920"
-   inkscape:window-y="0"
-   inkscape:window-maximized="1"
-   inkscape:current-layer="g8"
-   inkscape:document-rotation="0"
-   fit-margin-top="0"
-   fit-margin-left="0"
-   fit-margin-right="0"
-   fit-margin-bottom="0"
-   showguides="true"
-   inkscape:pagecheckerboard="0" />
-<g
-   id="g8"
-   transform="translate(-10.708266,-9.2965379)">
-
-
-
-<text
-   xml:space="preserve"
-   style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:200px;line-height:1.25;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';fill:#000000;fill-opacity:1;stroke:none"
-   x="286.59244"
-   y="223.43649"
-   id="text129"><tspan
-     sodipodi:role="line"
-     id="tspan127"
-     x="286.59244"
-     y="223.43649"
-     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:200px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans'"><tspan
-   style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:200px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans Bold'"
-   id="tspan30">ault</tspan>warden</tspan></text><g
-   id="g49"
-   transform="matrix(1.6845401,0,0,1.6845401,494.51937,-86.453367)"><path
-     d="m -121.83937,138.05241 -6.98995,-4.32717 c -0.059,-0.67929 -0.12536,-1.35792 -0.19902,-2.03579 l 6.00718,-5.60327 c 0.61086,-0.56747 0.88299,-1.41152 0.7187,-2.22896 -0.16043,-0.81796 -0.73431,-1.49379 -1.51545,-1.78472 l -7.67938,-2.87156 c -0.19288,-0.66309 -0.39344,-1.32396 -0.60164,-1.98244 l 4.79027,-6.65304 c 1.04651,-1.45048 0.20002,-3.49518 -1.56553,-3.78148 l -8.0976,-1.31708 c -0.3148,-0.61399 -0.64261,-1.21887 -0.97302,-1.81855 l 3.4023,-7.468656 c 0.34833,-0.757849 0.27736,-1.642349 -0.18731,-2.334963 -0.46103,-0.694895 -1.25061,-1.099512 -2.08393,-1.067976 l -8.21857,0.286187 c -0.42446,-0.531648 -0.85724,-1.056598 -1.29822,-1.574649 l 1.88879,-8.005242 c 0.19212,-0.81184 -0.0507,-1.665186 -0.6413,-2.254309 -0.58913,-0.589746 -1.44163,-0.832402 -2.25301,-0.641314 l -8.0033,1.886847 c -0.51953,-0.4405 -1.04553,-0.873315 -1.57789,-1.298218 l 0.28748,-8.217927 c 0.0312,-0.833106 -0.37268,-1.622579 -1.06668,-2.084574 -0.69281,-0.463867 -1.57665,-0.53527 -2.33496,-0.188609 l -7.46671,3.40425 c -0.60228,-0.332412 -1.20852,-0.657629 -1.81853,-0.975633 l -1.32034,-8.09889 c -0.28816,-1.76333 -2.32963,-2.608798 -3.78018,-1.565537 l -6.6576,4.790259 c -0.65496,-0.208119 -1.31382,-0.409101 -1.97854,-0.599674 l -2.87156,-7.680673 c -0.29045,-0.782461 -0.96657,-1.357799 -1.78537,-1.519368 -0.81672,-0.160726 -1.65861,0.111226 -2.22701,0.719365 l -5.60328,6.011731 c -0.67759,-0.07595 -1.35629,-0.142297 -2.03578,-0.199033 l -4.32716,-6.9919 c -0.94024,-1.519281 -3.1502,-1.519281 -4.09044,0 l -4.32716,6.9919 c -0.68014,0.05667 -1.3595,0.123017 -2.03774,0.199033 l -5.60459,-6.011731 c -1.2184,-1.309662 -3.3894,-0.87683 -4.01237,0.800003 l -2.8709,7.680673 c -0.66538,0.190573 -1.32359,0.392845 -1.9805,0.599674 l -6.6563,-4.790259 c -0.67616,-0.488791 -1.55773,-0.591006 -2.32781,-0.269916 -0.76917,0.319483 -1.3189,1.013531 -1.45366,1.835453 l -1.32034,8.09889 c -0.61138,0.316759 -1.21562,0.642619 -1.81854,0.975633 l -7.46735,-3.404903 c -0.75792,-0.346886 -1.64156,-0.275192 -2.33367,0.189262 -0.69451,0.461668 -1.099,1.251207 -1.06798,2.084574 l 0.28618,8.217927 c -0.53169,0.425714 -1.0577,0.858479 -1.5779,1.298218 l -8.00328,-1.886194 c -0.81164,-0.19019 -1.66401,0.05203 -2.25433,0.640661 -0.59063,0.589123 -0.83342,1.442469 -0.6413,2.254309 l 1.88555,8.005242 c -0.43773,0.518378 -0.87025,1.043255 -1.29497,1.574649 l -8.21858,-0.286187 c -0.83193,-0.02623 -1.61905,0.376856 -2.0839,1.067317 -0.46373,0.692824 -0.53536,1.576466 -0.18927,2.334984 l 3.40423,7.469294 c -0.33105,0.59968 -0.6582,1.20456 -0.97561,1.81855 l -8.09694,1.31708 c -1.76444,0.2877 -2.6103,2.33081 -1.56554,3.78148 l 4.79027,6.65368 c -0.20813,0.65693 -0.41041,1.31643 -0.60163,1.9818 l -7.68004,2.87156 c -1.67214,0.62818 -2.10219,2.7944 -0.79676,4.01368 l 6.00784,5.60261 c -0.0747,0.67644 -0.14113,1.35742 -0.19837,2.03645 l -6.99126,4.32717 c -0.70829,0.4385 -1.13899,1.2125 -1.13821,2.04553 0,0.83513 0.43122,1.60847 1.13887,2.04423 l 6.9906,4.32719 c 0.0573,0.68097 0.12357,1.36066 0.19837,2.03643 l -6.00848,5.60458 c -1.30817,1.21808 -0.87715,3.387 0.7974,4.01237 l 7.68004,2.87156 c 0.19122,0.66472 0.3935,1.32488 0.60163,1.98181 l -4.79027,6.65433 c -1.04696,1.45074 -0.19891,3.49602 1.56749,3.78018 l 8.09565,1.31708 c 0.31675,0.61398 0.6413,1.21886 0.97561,1.82114 l -3.40489,7.46671 c -0.34673,0.75837 -0.27507,1.64234 0.18927,2.33497 0.46169,0.69405 1.25093,1.09828 2.0839,1.06732 l 8.21468,-0.28748 c 0.42862,0.53398 0.85984,1.05756 1.29887,1.57789 l -1.88555,8.00654 c -0.19197,0.81042 0.0509,1.66238 0.6413,2.24977 0.58882,0.59119 1.44282,0.83337 2.25433,0.63935 l 8.00328,-1.88488 c 0.52034,0.44228 1.04717,0.87155 1.57856,1.29431 l -0.28619,8.22052 c -0.0643,1.78662 1.77543,3.01531 3.401,2.27124 l 7.46735,-3.4023 c 0.60126,0.33409 1.20748,0.65913 1.81854,0.97497 l 1.32034,8.09435 c 0.13262,0.82335 0.68292,1.51889 1.45366,1.83741 0.77022,0.31902 1.65055,0.21743 2.32781,-0.26862 l 6.65304,-4.79353 c 0.65692,0.20813 1.31708,0.41172 1.9818,0.60294 l 2.87156,7.67743 c 0.29073,0.78141 0.96664,1.35551 1.78472,1.5161 0.81718,0.16487 1.66121,-0.1077 2.22766,-0.71935 l 5.60458,-6.00848 c 0.67642,0.0781 1.35675,0.14309 2.03773,0.20357 l 4.32717,6.99061 c 0.43889,0.70725 1.21192,1.13762 2.04425,1.13821 0.83277,-3.4e-4 1.60631,-0.43068 2.04553,-1.13821 l 4.32717,-6.99061 c 0.68098,-0.0605 1.36066,-0.12552 2.03644,-0.20357 l 5.60328,6.00848 c 0.5665,0.6111 1.41008,0.88358 2.227,0.71935 0.81783,-0.16115 1.49347,-0.73509 1.78472,-1.5161 l 2.87156,-7.67743 c 0.66472,-0.19122 1.32553,-0.39481 1.98245,-0.60294 l 6.65434,4.79353 c 1.45159,1.04351 3.494,0.19593 3.78018,-1.56879 l 1.32032,-8.09435 c 0.61074,-0.31675 1.21498,-0.64586 1.81789,-0.97561 l 7.46737,3.40294 c 1.62589,0.74654 3.46791,-0.48334 3.40163,-2.27124 l -0.28617,-8.21987 c 0.53214,-0.42329 1.0575,-0.85503 1.57594,-1.29496 l 8.00394,1.88488 c 0.81302,0.19123 1.66505,-0.0455 2.25237,-0.63935 0.59042,-0.5874 0.83331,-1.43935 0.64131,-2.24977 l -1.8849,-8.00654 c 0.43903,-0.52033 0.87155,-1.04391 1.29432,-1.57789 l 8.21858,0.28748 c 0.83297,0.0315 1.62231,-0.37259 2.0839,-1.06667 0.46514,-0.69295 0.53634,-1.57789 0.18797,-2.33628 l -3.40294,-7.46539 c 0.33236,-0.60294 0.65887,-1.20782 0.97367,-1.82116 l 8.09759,-1.31772 c 0.82329,-0.13219 1.51852,-0.68305 1.83546,-1.45431 0.31921,-0.76971 0.21708,-1.64977 -0.26992,-2.32587 l -4.79026,-6.6537 c 0.20683,-0.65756 0.4091,-1.31772 0.60162,-1.98244 l 7.67874,-2.87156 c 0.78166,-0.29074 1.35584,-0.96697 1.5161,-1.78537 0.16462,-0.81698 -0.10792,-1.66069 -0.71935,-2.227 l -6.00653,-5.60458 c 0.0728,-0.67643 0.13983,-1.35675 0.19837,-2.03578 l 6.99061,-4.32784 c 0.70862,-0.43763 1.13964,-1.21137 1.13887,-2.04423 0,-0.83317 -0.42927,-1.60781 -1.13758,-2.04553 z m -46.78525,57.98791 c -2.66929,-0.57561 -4.36685,-3.20652 -3.79384,-5.88425 0.57106,-2.67449 3.20002,-4.37661 5.86864,-3.801 2.66929,0.57171 4.36945,3.20718 3.79709,5.87971 -0.57236,2.67317 -3.20391,4.37725 -5.87189,3.80554 z m -85.20366,-67.75316 c 2.27683,-1.01282 3.30259,-3.67866 2.29205,-5.95646 0,0 -4.7154,-10.47937 -4.57381,-11.59289 0.9898,-7.7854 6.88718,-15.019243 13.36396,-20.688869 6.66345,-5.833 18.89505,-8.222474 26.68826,-10.333706 0.54928,-0.148782 8.42735,8.001345 8.42735,8.001345 1.71813,1.803314 4.57395,1.869198 6.37337,0.146991 l 8.51906,-8.148336 c 17.85049,3.323592 32.96406,14.435192 41.67369,29.680135 l -5.83221,13.17273 c -1.00685,2.2797 0.0208,4.94572 2.29398,5.95581 l 11.22868,4.98735 c 0.19381,1.99351 0.29593,4.00716 0.29593,6.0488 1.5619,14.92169 -5.31066,30.62323 -17.46088,42.66296 l -10.4599,-2.24782 h -5.1e-4 c -2.43617,-0.52245 -4.83397,1.03104 -5.35287,3.46799 l -2.48197,11.58184 c -7.65856,3.47579 -16.16266,5.40686 -25.11946,5.40686 -9.16167,0 -17.85115,-2.02668 -25.64565,-5.65076 l -2.48064,-11.5812 c -0.52164,-2.43708 -2.91775,-3.98961 -5.35158,-3.46669 l -10.22443,2.19579 c -10.18477,-11.48557 -16.9061,-29.32695 -17.17146,-42.36897 0,-2.21009 0.11901,-4.39417 0.34796,-6.54053 v 0 l 10.65111,-4.73302 m 14.02218,67.51249 c -2.66994,0.57626 -5.29824,-1.12586 -5.87255,-3.80035 -0.57105,-2.67903 1.12651,-5.30864 3.79579,-5.88425 2.66994,-0.57106 5.29954,1.13106 5.8719,3.8049 0.57105,2.67318 -1.12781,5.30733 -3.7958,5.8797 z M -258.679,119.27181 c 1.10764,2.49887 -0.0195,5.42767 -2.51513,6.53337 -2.49564,1.10894 -5.41662,-0.0195 -6.52362,-2.52033 -1.10764,-2.50083 0.0189,-5.42442 2.51513,-6.53337 2.49719,-1.10758 5.41928,0.0211 6.52362,2.5197 z m 51.05194,-46.335823 c 1.9727,-1.889433 5.10182,-1.814638 6.9906,0.160653 1.88489,1.981149 1.81334,5.113514 -0.16389,7.001671 -1.9727,1.890086 -5.10182,1.816585 -6.99061,-0.162619 -1.88437,-1.979444 -1.81107,-5.110655 0.1639,-6.999705 z m 57.87539,46.575823 c 1.10375,-2.49856 4.02585,-3.6268 6.52231,-2.51838 2.49627,1.107 3.62279,4.03449 2.51514,6.53337 -1.1033,2.49972 -4.0263,3.62903 -6.52361,2.52033 -2.49301,-1.10765 -3.61953,-4.03253 -2.51384,-6.53532 z"
-     color="#000000"
-     color-rendering="auto"
-     dominant-baseline="auto"
-     image-rendering="auto"
-     shape-rendering="auto"
-     solid-color="#000000"
-     stop-color="#000000"
-     style="font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;isolation:auto;mix-blend-mode:normal;stroke-width:0.65041"
-     id="path2-6" /><g
-     aria-label="V"
-     id="g6"
-     transform="matrix(0.65040957,0,0,0.65040957,-287.20599,56.842282)"><path
-       d="m 40.368,56.262 35.459,-17.039 51.802,139.85 47.083,-133.45 40.067,10.639 -61.629,166.39 h -51.153 z"
-       stroke-width="0.21397"
-       id="path4-7" /></g></g></g>
-<g
-   id="g10"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g12"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g14"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g16"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g18"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g20"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g22"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g24"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g26"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g28"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g30"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g32"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g34"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g36"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
-<g
-   id="g38"
-   transform="translate(-10.708266,-9.2965379)">
-</g>
+<svg width="1365.8256" height="280.48944" version="1.1" viewBox="0 0 1365.8255 280.48944" id="svg3412" sodipodi:docname="vaultwarden-logo.svg" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <sodipodi:namedview id="namedview3414" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:zoom="0.95107314" inkscape:cx="683.4385" inkscape:cy="139.84203" inkscape:window-width="1874" inkscape:window-height="1056" inkscape:window-x="46" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg3412" />
+  <title id="title3292">Vaultwarden Logo</title>
+  <defs id="defs3308">
+    <mask id="holes">
+      <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect3296" />
+      <circle id="hole" cy="-40" r="3" />
+      <use transform="rotate(72)" xlink:href="#hole" id="use3299" />
+      <use transform="rotate(144)" xlink:href="#hole" id="use3301" />
+      <use transform="rotate(216)" xlink:href="#hole" id="use3303" />
+      <use transform="rotate(-72)" xlink:href="#hole" id="use3305" />
+    </mask>
+  </defs>
+  <text transform="translate(-10.708266,-9.2965379)" x="286.59244" y="223.43649" fill="#000000" font-family="'Open Sans'" font-size="200px" style="line-height:1.25" xml:space="preserve" id="text3314"><tspan x="286.59244" y="223.43649" font-family="'Open Sans'" font-size="200px" id="tspan3312"><tspan font-family="'Open Sans'" font-size="200px" font-weight="bold" id="tspan3310">ault</tspan>warden</tspan></text>
+  <g transform="translate(-10.708266,-9.2965379)" id="g3410">
+    <g id="logo" transform="matrix(2.6712834,0,0,2.6712834,150.95027,149.53854)">
+      <g id="gear" mask="url(#holes)">
+        <path d="m-31.1718-33.813208 26.496029 74.188883h9.3515399l26.49603-74.188883h-9.767164l-16.728866 47.588948q-1.662496 4.571864-2.805462 8.624198-1.142966 3.948427-1.870308 7.585137-.72734199-3.63671-1.8703079-7.689043-1.142966-4.052334-2.805462-8.728104l-16.624959-47.381136z" stroke="#000" stroke-width="4.51171" id="path3316" />
+        <circle transform="scale(-1,1)" r="43" fill="none" stroke="#000" stroke-width="9" id="circle3318" />
+        <g id="cogs" transform="scale(-1,1)">
+          <polygon id="cog" points="46 -3 46 3 51 0" stroke="#000" stroke-linejoin="round" stroke-width="3" />
+          <use transform="rotate(11.25)" xlink:href="#cog" id="use3321" />
+          <use transform="rotate(22.5)" xlink:href="#cog" id="use3323" />
+          <use transform="rotate(33.75)" xlink:href="#cog" id="use3325" />
+          <use transform="rotate(45)" xlink:href="#cog" id="use3327" />
+          <use transform="rotate(56.25)" xlink:href="#cog" id="use3329" />
+          <use transform="rotate(67.5)" xlink:href="#cog" id="use3331" />
+          <use transform="rotate(78.75)" xlink:href="#cog" id="use3333" />
+          <use transform="rotate(90)" xlink:href="#cog" id="use3335" />
+          <use transform="rotate(101.25)" xlink:href="#cog" id="use3337" />
+          <use transform="rotate(112.5)" xlink:href="#cog" id="use3339" />
+          <use transform="rotate(123.75)" xlink:href="#cog" id="use3341" />
+          <use transform="rotate(135)" xlink:href="#cog" id="use3343" />
+          <use transform="rotate(146.25)" xlink:href="#cog" id="use3345" />
+          <use transform="rotate(157.5)" xlink:href="#cog" id="use3347" />
+          <use transform="rotate(168.75)" xlink:href="#cog" id="use3349" />
+          <use transform="scale(-1)" xlink:href="#cog" id="use3351" />
+          <use transform="rotate(191.25)" xlink:href="#cog" id="use3353" />
+          <use transform="rotate(202.5)" xlink:href="#cog" id="use3355" />
+          <use transform="rotate(213.75)" xlink:href="#cog" id="use3357" />
+          <use transform="rotate(225)" xlink:href="#cog" id="use3359" />
+          <use transform="rotate(236.25)" xlink:href="#cog" id="use3361" />
+          <use transform="rotate(247.5)" xlink:href="#cog" id="use3363" />
+          <use transform="rotate(258.75)" xlink:href="#cog" id="use3365" />
+          <use transform="rotate(-90)" xlink:href="#cog" id="use3367" />
+          <use transform="rotate(-78.75)" xlink:href="#cog" id="use3369" />
+          <use transform="rotate(-67.5)" xlink:href="#cog" id="use3371" />
+          <use transform="rotate(-56.25)" xlink:href="#cog" id="use3373" />
+          <use transform="rotate(-45)" xlink:href="#cog" id="use3375" />
+          <use transform="rotate(-33.75)" xlink:href="#cog" id="use3377" />
+          <use transform="rotate(-22.5)" xlink:href="#cog" id="use3379" />
+          <use transform="rotate(-11.25)" xlink:href="#cog" id="use3381" />
+        </g>
+        <g id="mounts" transform="scale(-1,1)">
+          <polygon id="mount" points="7 -42 -7 -42 0 -35" stroke="#000" stroke-linejoin="round" stroke-width="6" />
+          <use transform="rotate(72)" xlink:href="#mount" id="use3385" />
+          <use transform="rotate(144)" xlink:href="#mount" id="use3387" />
+          <use transform="rotate(216)" xlink:href="#mount" id="use3389" />
+          <use transform="rotate(-72)" xlink:href="#mount" id="use3391" />
+        </g>
+      </g>
+      <mask id="mask3407">
+        <rect x="-60" y="-60" width="120" height="120" fill="#fff" id="rect3395" />
+        <circle cy="-40" r="3" id="circle3397" />
+        <use transform="rotate(72)" xlink:href="#hole" id="use3399" />
+        <use transform="rotate(144)" xlink:href="#hole" id="use3401" />
+        <use transform="rotate(216)" xlink:href="#hole" id="use3403" />
+        <use transform="rotate(-72)" xlink:href="#hole" id="use3405" />
+      </mask>
+    </g>
+  </g>
+  <metadata id="metadata3294">
+    <rdf:RDF>
+      <cc:Work rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title>Vaultwarden Logo</dc:title>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Mathijs van Veluw</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:relation>Rust Logo</dc:relation>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
 </svg>
diff --git a/src/static/images/logo-gray.png b/src/static/images/logo-gray.png
index 21caaf52c43a4a64cd7d365e64fbfd54e0756453..8fb1148ee0863de197f2148401500882800cf7eb 100644
GIT binary patch
delta 2395
zcmV-h38eOk6y_3;B!4YQL_t(|+U=VKaHF{nfd6urnR)k^nejVjW@cE1V`k=MW@cvQ
z%`!7Hm)mRNH={{4_Dk-y(>C`g(xR+5{=dkw>^k1R7GCBGJQ{F-Gw`^O#&{8ys8F4z
z#FI=0X<W<<0xaenPLM-cVtIqQL?J~=cn-r#`eQHzFqRHsF@Gi^GaxFwKp)Pervv;b
ze{j&T4C55K)08N%;uOBcgy_3?G;?hPFm#B*FD9V@&r*+07*#*>>BMn-+|7Ja=)_$7
zR(n5c^E9Usg;!MJDJQ$9^9{1Bayt7k9OgXvUv0i1)tLdW6&Ze1sN=w&fVOZ7zi^iI
z|6UIKGm8X2Du2Apw{#&FqnblS!uuw2jDQ8d2j3QV^+Qb_5xt%NL|REQwz3pUZyL*v
zzD#)E`@;B+C}#NgVno;fN5Zd)!EknQ!9QIwuGNU1MtI+W>ajszbzu~KVW;>@^wBNU
zqgJg`i%{=Uq^rks^u~w|<Ynp-QTW8uo56g|{}lYD7Ju&ABy&PDdMiS`pHc5D%2Cx?
zC*F-P{5!?6=zj}-9Lq4|unEHpcy!aW;v>|1JOPb%Pv*&HvN3ERF~aZ%{V&2V$p8$m
z;nAOMBmzeDbX?ym{HbmC5XV|ZA+v!Il*#j@1b6TW{Tahh+VhOIh;S>fik^?!cra*d
z#9zSYe19^<vYl6h8c!t}QVaK48q&}W0~*kfQrIQ+Z9Kv~|GYV*JPnZwhBDm6Q`|uU
zb}#qwBsWnKk1FBdQ--T~oZG4A+9|93mgW|o;vp^Dd7Uij>MKex4a23xvPYx)Q{0Z4
z=&U$KB4=WlPboeSTQ-uW_=!}EqsMp*?StV#>wnvp%@?#i9u7Wb_(eW+S4;5<h9ksU
z{k$hFPuZx&a<24KnKrD&sIKBl+}F0j;|)@zH8<}=<V>dPYmU>3(k^@nx1UK-HWN7o
zH?4m}65o@iy)5SvJdaCBQBwxr5r@YQ+RYYR`1AB*X@^G)pIM;OxmF&rBPK-s+)Ux&
zKYy<MxbW-A-xpc^EEdZk%kn&iy~H51M@BM-pZJJwoWzhqBW(CT@CJq~<}j9th@JB|
ziXoMLd_!B-Vfc(J8$RV&ARayWh_>vLkkD3vKalYl7SoPj7%Ox5B#mT9L7)?<fw*%4
zM)eZwLFy4u`&Hy6qEWb(`s9$wFk10EAAfTgBRaPT@NZxWtAm!4ObKc-9_uAq?LGPi
zayW@59{OVFq5pf6`e>EM9t^)Y@O@-5l4@8hqP+8#yktTQA7R|cmvGxa9rEhI83H;M
zg^!;s?p6OYIfkKw$0^Lkh~8km<!U`Z`^mDtuQOsNG1}GTL93555eraWTW`jQ=6{k*
z;oyt#G$wSx<v05vL{47YvJk^<ifX_18X|m!VIgkHnnN76&pl$j*0IgE;d2^(0?zzv
zB=oMc;2XYldiVmvMvns;&6lH%ysMb^ql00M`hITto`cb-j3s0%uCJ+G{P3mxOw-&v
z1H(gtv^_dD{8SDPpBE&iUHI3yzJGL*wZXN$X~P#*%UutaV))L1e~RtF!x+*%PGL@%
z$ggW19izF=sM^PDmxV-r99QusU(<;m3|9CJA^^V>S$T0u6>`a;JXc}Z9nA90B>{v3
zUxDfZafbu{sD_Wdiga^rT<5?aFNv*!qa!8HD|~X?ez^vN5vT;TMo?jvLVxRm`(qZr
zTmSQ*o%4yIDF-l)<_~{qA>fNJEw3iyMGTYih~+@uTE!R)U!dNc!I#Y9ptW|n@K@rw
z!Zz(eh0`4P%P?Vess(?i+XGi)Fm|=`jf3=6O*|(^lFDX>4!MQn`LSK?@C!zDAIq3o
zpvYfWX!vhqm@S~kVt8Ht)qfR9j3khP=4)d4mRw1Wv+2Y4e8_+h_)9|I@7L#5p`rtS
zD~3%>3DF-c_-n$#*E(O99-Wri3h_QXuKlYsA7)~-&abcOYPhcc{_ya5&w_un(C{yi
zcP>R4vhY(KStOAdmIQPnZ>vNL47s$iDxoI}0Dp_@fn3`u4*W&#et*c4eBjHjP;V({
z8RkGT^v9#E&V)&hR}G1#;0~9kbjMIaOHt#(X9!00H?%sy6b`=DGq-XdhB@SIdD00`
zn-3%i>;%%@8T;!6fIkaEPuI%zHhkeUu(0ssG~<3}yYP@ANu?zFHGMqmczrFl;Moo@
zhh);%L*Tc^h;Ak>Xn$#20Qe6CE!&B4AB5hr?fC;kOZ6U!;YB7$A`%PnGXBS$v3Dx~
z{2wuFb>EW~Hhg*J<CLaQ@by%a6l!1>iF&@tx7IGWjO9~{bmxx49XdZ=B#EjtC(VKH
zzTbEr=3#na3IP8~^=Z!?Ji+&zip~ipu-u-xviSCrWY$1!X@9yQ6XA3+3IP9742CCB
z3(0J9;8)<JjH(du<vd@{<%imzF<tb5^<;$sb~l=^dLrlfl$3$EdeDVWb$Mu4Pvi?f
zks}yKFAe%uBfxQT@nN4k`UU+)%QhCn0WL&lI}fp!P5HssS(L^z$lAeKOydx_0^dDm
zAIy2k9HS1;GJlrx<Og4)z}U+mrHIGl290C+EqWIoamXa2W#~_`cu_o6`G%WZ_;MdE
z*YTOpb@{+|5||5Li@o=8PNBs12WsJ5Qsg<du|h&*4wd<{0PriaD3}*EOCY6kDVuHh
zvbWC0NZSD|Fe()kzD|7ttYQy_g?4$VZAVBzYHRkfl7B$BpW$*lGsqN=89XArOvHze
znwHO143o%AVG*y8L@YDJ=T`EzF+S0u?kdEb;GxuNent1KG?votRLA!@v0d-jPGx}1
zA3tk(R|@DMj6sj!@X?3xR|Z<KT<{*I7pLIy8$KF3eV@gzY?9F(V?7<YMbCB~!^dFU
z9?!>z&wswX&^Z{7S$xPO43*K16*(Buwvt->%;3FXw)fLL-0cP8@j|2!KA|GhzX(Di
z5qu>H6zSg1cs$NTPB}Om<LDK5T!`UfJcbtAy`8A=6n+8($lx7DW0*z*JXUEA3rNXT
zR~Dd2v?f(r{Ka=;EqYh8r<3CsGXtYKmj%vU%~Zb8yPA=>r5wP7*XS+f$luTJm=F3P
zk-eWE{btGv`f7~p-wa|=^qVPv-*>VuVg>;gadz}OS%0HM_S=Y2`3KK&Y5LaFAM*eJ
N002ovPDHLkV1hr8u4@1Q

delta 2559
zcmV<b2>|xy5{VR$B!AFJL_t(|+U=VKoZGk(##3fyX51@8pPU>sGv5`CnVFff%*@Qp
z%*@Qp%<XpbMyjohz1hqArJH+jkrtxySQ-B#jWlD&_tyn4T9(gOw+i;4y3Z#dp7X`K
zB#OTXUb``KQf{2U5gEXEgfPV~aR{f8@5apX&0y&Sqqr5kNPoEJh!zN?s;w~-A-tAg
z6o(=jpd)Ibl?(U=@eTI4B<-yvbigwSUg=uUDD`NUPm=t@)N(XW@Jgprr<7<YD!W|G
z5jhB-;;@*2d$152#Yb{D&V-MPwx}c;SY;CIF1v_^40|#<;M&JBDY?7?dr7j|UsAQ|
z=EwVP7bKoduz$PkA}TYaDOSbskw8^^jl$tJTH_egt)5_axugD1&m#METDp&A@+`h>
z@JD(tz5a4|OjZ9a|BbY6%&u$;=1nj7ol-5Pn|&D~>*DqZOK-P0xxl&qjqroTlx_Z+
z>C_$h^%O5qc}*JdyQGG1)lI;Ub(ixJTH6=P9ZLH&rhnW~rnAY}_lTE+rQ-|#svNQ}
z`pYl=ui)eDFie;GIfk4Ov>$Vg`S;mOc`v^3QC~~O{$IhrF|(MZFY6rrcEKrkaOEu+
z!qD$RSMz!vvcwtwxc@`=*W{5c5fZ~FVfHbOVgQ>5qq&~F%Rd`FPUoGKuhc4l#me!7
zSAXP`_kW8YMIVe517%Cmpmr|ahrC<7tg4m7U_X9IhO%kFx2{IAmaMWhU-bEE7qu#)
z1n)a3w^h-qcw=TR3skTwUXzErS?g9jChw;$at8OcSryeaDEs}l$a=V)4<}JsK7jhx
z4VAO0_hs-o$J<b!`Yr30BcJ{LWS##O(Lg>TYJWI~Ud1WL!;1#={|_E&)hQJr;by96
zOeS1qX2WR(gj&sYkGvib5AhLfpZx}tt+h4pi3Gn^0g9zBdip#^XML$w9hZGS>z?lm
zORO?VA_JE&h|jTx^|9Kzfeng}>CBJle?9EgM=vC0wR^3F8Rnd$_%N8)3qP1wwq&vY
zIDb)@*+i~cd_x)Hri{yCzf^A-z+p!~r*HJv#13ZOhaUKo*|=YTo9NsFa<(lwBCbqp
zH-rn~y4c~%SL~Jtz6@vx<-GlfPE7I)Ne=ol+QDLqo;!W;2mKyGh;UTr@X<)TL^@|N
z1*04X=dXh!bmRnj<5#rDW;M%kp2J5zEPvF4uODf2#D03cFHn`k$4KfighjF)no`ea
z^om+ZjSb6&F<(5a5(@Hnc8M!FdVfhIGx%@P1k5Ggzy^D}d9^)#tB}?i48eEu1@R3I
z+4|#ShmU*21T$g}iV3R9F*@8k#M2uza6;5m3ii=;*ah9l(!Nehyes|YNC_My{eM;*
zJ`JeTa*S1+uZUPqTI*5PX^Y>q9UOi)r4guLT=QJ+FzrprDRHlPr)oH%K0@+k4h(r$
z_!3wZ4ABRC<T(ikd7CYnPUhH>y7|rJ-fJH{>&qA$d|UE{YI_%4^rwEho7R_Vf@2v%
zY@X3PPV4@g{7KDs4qpvHayG2s9)FUi$N&>%z+4WWP^KWoFPW=L(#NlN!T*%^sz1Q>
zK5R5NZ#QqakU?(73|YkFgUxum%E-1FRkqwXN3SZF{_w?1B-z32BkVdU{-?=G5jwVV
zrQTC*bHi7w0552-=7UG}x(oid)LQ1zP_Fw24&S{o5KpMNpgy-r_Q-$1oPYWjgM82|
zJJnq?;z6S=Y{?W+!%WTV2-Yv;;kt@}&KbdC=?~vuPRZ(4(bK&=y*`<gRo+eiH`@Ka
zMz2?-#*QqsM=Ps8x}7fghn)Foqzibs$%mVPKZaV}Y8BuWb4b>k!QbO~|C>|~+~@yj
zHfM!g8_Eal6TaggN#$(Dlz&@XK0hNq!~yT}7#04y7~q?aO}?Tbyr(wIYNLo#B)dY%
z^v1c7%OU(qV2&`m7<@K@O~E$h%G@%mB8RWmgot${gTE>we74(}899`agE%1XJuEg)
zjvz>_Hb&<nc43j6_HV)87b|?x!dXz&c2GX8UkxR_FZDn;VB9G!&wsLs-msn~&0hEu
z-^spS_{_m@%ii|DFYbcBN#AIl;H~l}27gIJ_-3C^uxvb3#6#Yt6+zbyAD$1&-sh{u
z$8Pn%v_GhvEe-hmk}@JWX88A!Y_Ezs3GDe=1+?s06>$@!y2`4na<Gwofl%b*K0hk_
zO~xmfLJp3@U&1wbiGK#)M7X-XZX(63+;yliil(N`Cy{bdYS(H<xbk_E7qX*6l;!Y^
zk5u(%V@fTAVuVlDt39_>Q9P%D8@s>4NJcVw-uJmawwdjN=`%WfF;yj9{?g|UqSlbu
z;G5hI)lK2RoZrh8^W8U4#7Ev6?#k?>tSm{l)?CtiUihMqEq__&g!iITRQM*gHkV<F
zc5L;PcNZ@k?J3lH7$a4>&kUy&YSnWg=ot;Z9rbLmE#)`~K6AS#Y88kHKF?}~?@0M&
z>g=U)Po>!eJj;VF`G}(<8dS~}EGFK?X)k<y8s4tTN2n^#8-3*8X>|S+chd1@{*pGH
zF9>uC>_u|zqJQCFN`+4<)-)NU40?us!p3xMoI`Z@etm8ATuJo}96kkA$Ee-ammvoD
zl+S<JER#C}g^a>V2E`zDP2?=jHWlsVD+=ya1??4t*L@tm-X5M$rE@gu$j|5P*~y>i
zB*Ot}PxW}^!z>1%B1UVy;3dmtJ?zKMDDX`nhg0%tHGi}9Ix>YUN#+r*a(ProAV`rl
zMg9JoG#e&SToV<(_fFPOUxvpri5h`I%9u!1)(aH61QKodoh&v9x%N-uUIX9gc<!5x
zlea~Kuj1Fp@W~|}<O{f222Ga7jPw(BB78MYDbSR3yBH0=Nf%HIflWGDa++r01`c1(
zv<q720)LJ>_SwTYI(&3wm$gJ2xrS$QSUpC@?56+QF*))*T=9^cjSET><HfydO(D&N
zSZI#~I{_ibXfsdKg}p}jvWQtq2iMxi?@}$QU0p8Vd6FobPGC9yU@7cIO4t4TkbG0g
z<lS6`>awq%59hE_exV{23<;;r<<q3!!xy1Y;D7cW7HcQ92VGUlOt!>ACzoTA3~jJc
zU-x0GMjPu^CtMQ^$dcZkC0i=%oKv4dBlGkEO)>m}5RHqm5#V>wtHF2Tb2EC|ppkes
zwZXoqCAQlyyyw67d}N8wHSy<mrmOp<2+>klF9Q5)#Tz(if{Vm+JL!DuE*Xt<;51h}
zgm@z!^jqZJSgdvz^7q6OYw>+nQ+dbpID=EVp3$#Ef9tcFiKmoJO+2L>-y&Z?7yf)c
zukPpbMJMrcg4h4oH&YUYeJAU%42TITeiXn#=Q~++ZzxgXUGR+kZNv?UZzHCE`4`;M
Vyoe63+T;KL002ovPDHLkV1fWd7U2K@

diff --git a/src/static/images/vaultwarden-icon.png b/src/static/images/vaultwarden-icon.png
index 36c8c79eba23c3b769a54b7a88efcbac6e9daafd..3a6c066854659172c2ba9cac811e1910a48aedbc 100644
GIT binary patch
delta 1442
zcmV;T1zq~F2eS*1BYy>YNkl<Zc-pkt1GFny7RK?NN#8sT+jeK{HWo&0dtq$bwmoex
zv~AnAZ7ZGonBOR=m8w&f)YE^zcU9+{_1$Ej^X+|ZnE%+=+xuTNo##Bw{&#P;@%*ng
z-xIuE*UQ!w!WY07gs`rcb-jLZ3E0v1w1n^p@U*sNJ+G(dd4J1ldm4N~2rb{U<DwOC
zC$6@Za6X{xIRl>81)MLe<!aoyC<RQI4Z5y1j%#$mY!eGBV5%eh$dW=^6_0pS6>VWj
zKXQbr3nt(#pkp5wDCa;;O$RIIJbUYaw=9@|)r@1K{<a*)Y6~V{N!82+-x9)ivY%A3
z<opTP*VX=HTYtMt>ABc9yvKBBV4kBLV;;^h!@9odQavfV+SZ?3ZQptVu2Rm+{$Q=4
z6g|U$GltS^ZGZH#a;~Z;;7`i9eDFdh63$4ne5s1G{Zl;w+bZK8jbvoY$9zmnByo*0
zwjC{CsyAE18|<c>4Mfd&lX7<P25We;spSHWFq*&C7Ju+FqdB5nz>kgQan1V9F_Isb
z3s_ReXg(ln#QH|lv1C;e?roUk)1$YRfWIhmqG9$bPr?c9+~&E;G>^;kqG@7Gyvif;
zJZVO5SDdG9A}?S^cba3EIo>J!Rgs;>3V6RFe-Ykhc47TH?I;uQeMOEG!g`7vJyyU0
z3iQjjV}BI+zDz*N)fI>49r8?iofr`<&&qRG|H<X@T&*>n18Ze<%&Beyn^$bjkBq4u
zi~L9kTY#IKn$=co6_fB<u(Qx`60YnE>&bJ*NC8JFvW^h0#0eV0E_quD;q^X|<^NP6
zd=6|Sgv*jjU^!6{uhEs~QXy;wJ|l!_fb)IA>wiVqNVo6;x10@rlGXlszEmmTBZ_=p
z2tNU5YY87Cqidt^HXSm0u5dqireDb|W41h(RSGymo;jB7SC`L#`(2?&M#tMiSl81@
zuK&&Q*|Mj|)3b(<#xmx}b8_}<d4Dr0&(qc&Bw<^3X!*UYoLhL$(jA<}mlW9}_k3l0
z>3<o})fQr){dogUO%|T+C3$Wu6>wQ%Ix~_nryKB`WrPS#U+}z)vcl0}io8Qw#P{Sm
zbj&&Vyf0{m^Uxbzx7l2g-y~yxpvZ?r5`WI+66e(u&`f;s!K6aGIq|?H>Ig{Rg8gcG
z{3f~Q?=1De8gD_Ggynv>t|I@;J^yW~2Y;q<Y?6e@HoT#9V(N=e4Yl75_op6M8aF4~
zFgb``sVF^DU)&;lwpoP-rnxkk9z^-07~Wetd94xKY_HS<Yn7`VZ!4M+d{fjqXkId1
z2upY}^}y2jE)>kDZ^!48aY@DW)E9Tl%r?&#Dpt4>iguiKb3JR9PEURDC?Omo&wp&o
zl+JIbC0|;S0e?)Vm-^x>rkn6=r3aQCLTO2s7Nwnv-y!wI?QK!&foUx5<?5pJ`-=Qo
zh#J0lsQpVlFpa;ZrarzgN#js?p7T@<4-9WNCv8lU)-1Odt}#U<@gXVWJ#eb)$T&ya
zM4jgJ@8f;3W(&Vp+ML#F&%dH!b$^@tnqS(W(ih8eX9K_VHJe*q!{_R@=RcM*(|}2<
zRQO_gjIUzSfLZk~D2`ChOXh(+D||8az&*h{FDd7U(U(-a80Iq?-r@=TtUvBYWc2i;
z9{4jn?kyTVYnUC=OR90#bdL`0;-jj3G4a5oWLH|bq}_J@FDhqRL1;J|w|`anV&Z|f
z<17tf#as^jX+f@P@9`t2W;wo2>WhUwu$}FbQ~bz1Bo<WrvU*o#T;@AIWVRx6Z0@@*
zSH@1sW%WW|*Uw9TvhdgSX|J=WSZ(3{WV4rZEeL1Z%fAPmvZ2YO6#@{Rw4&a$-D!f>
wo<dC9(Qo4N9kz+fVKH?}{~O6``cK~j06^Ylb{$k>-T(jq07*qoM6N<$f;)uYfdBvi

delta 924
zcmV;N17rNN3$X`~BYy*UNkl<Zc-pks0c2!X6vpw-Sn<?~5i1r|Ys89(sH%z>MU9H8
zf*{6-5iDx4M#Ngwf?8EYu_B0y2q3Cf6-2FyT1Bl<1%p_%7OSc%s`g(VlgxW_X3p7b
z@&iQXzIWfb$2s@t^|8%{a%2Ex%F$w5mya3%9ZBL8fM=3K2Y-M$T@DWThpHs;6ml(=
z>06-*xvnHp<%|PO66Dq-91?5fG&#(WVrDXrU6g2(qAjsV`m3;x=VwX6*Hj8t@ytpR
z_wjtB6ugILH~0mXQ%?L5kFo7=oq5VCEO3bJF-PPS!6?tE>m1h@pmV&*Z;Ye}Y?YD8
z`L{~A3N}S>gnwmPL&!MfQyH)wd3oQW!3xJ%4iv^bwqsUk&|*dw917!U(pq?y6og9_
z78$Rn5bAgqrArI~f2HleN*hm00r=Wf%k4&xT(pSVS086YWpZHKW7wJcm}c0wSiaU+
zRIoj88Wz?F(pcOhYE4ChDZ(=VPO;ob2*5W0JYtH^jem_$qf6h(z=wQ+<%vcj^S>Pz
zy3{1GHT-Y9lNJPau)G|srxe|>(&h<$yI4Na2-xcMJv6&Twy@2UVUmeiOcEVzTgr(I
zie9mx9JVVm@I99Alw+H+4fU}+s`X$K%V#oRyQXtAj8>J59tDwQHH0}#&T4PS{mCC#
zuB(JDmVdk6cKjDu!tz5}E6Wg(aY{jK=};5$%314_%s7O>E_o%;gnXzH;}9A(yru%G
zSpG~ps@9d24n?~v!Q|G`pWIaK*c}WiVGGM`=K#KC%$LfV_TrV-`36+N0obSPjwEjI
zi;=wr{5$%(#$7C5x&e5}f-X2gcz^VD4cnATxPNIs!9GTBM}rWrm=ragL{kYrVp-xL
zmUV0YCDri}em7L@dkOnD+VcI#BE<c!67E~AYI!=aNzoT7-~mzdPC?s@P6%#l09Oc7
z0n?O7@#}e&TXX^VRU&4O3eSQ6_=*}+;p;h`RX!%-DtnhRAvuu<NS~0*(56A1V+QR=
zRDZ}xbn)Dh$Qf1Qm^uyG%*4lZD90gy#2k+EL-&Cs(X_^N4vOv#{B2fZycZI;FCNB8
z`lQs>;}PSm(_@UJVRbrtSe-tuPq(ZTtTHL9O2Iml^8ccS=A;$n({{sR;u3S}_6jjm
yw?Ng4Xf=zhLjTEY%zjcB*6I6*|0SfG1{DB1H0x%#5bv-60000<MNUMnLSTY!bih9V