37 changed files with 1586 additions and 14 deletions
			
			
		@ -0,0 +1 @@ | 
				
			|||||
 | 
					DROP TABLE emergency_access; | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			|||||
 | 
					CREATE TABLE emergency_access ( | 
				
			||||
 | 
					  uuid                      CHAR(36)     NOT NULL PRIMARY KEY, | 
				
			||||
 | 
					  grantor_uuid              CHAR(36)     REFERENCES users (uuid), | 
				
			||||
 | 
					  grantee_uuid              CHAR(36)     REFERENCES users (uuid), | 
				
			||||
 | 
					  email                     VARCHAR(255), | 
				
			||||
 | 
					  key_encrypted             TEXT, | 
				
			||||
 | 
					  atype                     INTEGER  NOT NULL, | 
				
			||||
 | 
					  status                    INTEGER  NOT NULL, | 
				
			||||
 | 
					  wait_time_days            INTEGER  NOT NULL, | 
				
			||||
 | 
					  recovery_initiated_at     DATETIME, | 
				
			||||
 | 
					  last_notification_at      DATETIME, | 
				
			||||
 | 
					  updated_at                DATETIME NOT NULL, | 
				
			||||
 | 
					  created_at                DATETIME NOT NULL | 
				
			||||
 | 
					); | 
				
			||||
@ -0,0 +1 @@ | 
				
			|||||
 | 
					DROP TABLE emergency_access; | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			|||||
 | 
					CREATE TABLE emergency_access ( | 
				
			||||
 | 
					  uuid                      CHAR(36)     NOT NULL PRIMARY KEY, | 
				
			||||
 | 
					  grantor_uuid              CHAR(36)     REFERENCES users (uuid), | 
				
			||||
 | 
					  grantee_uuid              CHAR(36)     REFERENCES users (uuid), | 
				
			||||
 | 
					  email                     VARCHAR(255), | 
				
			||||
 | 
					  key_encrypted             TEXT, | 
				
			||||
 | 
					  atype                     INTEGER  NOT NULL, | 
				
			||||
 | 
					  status                    INTEGER  NOT NULL, | 
				
			||||
 | 
					  wait_time_days            INTEGER  NOT NULL, | 
				
			||||
 | 
					  recovery_initiated_at     TIMESTAMP, | 
				
			||||
 | 
					  last_notification_at      TIMESTAMP, | 
				
			||||
 | 
					  updated_at                TIMESTAMP NOT NULL, | 
				
			||||
 | 
					  created_at                TIMESTAMP NOT NULL | 
				
			||||
 | 
					); | 
				
			||||
@ -0,0 +1 @@ | 
				
			|||||
 | 
					DROP TABLE emergency_access; | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			|||||
 | 
					CREATE TABLE emergency_access ( | 
				
			||||
 | 
					  uuid                      TEXT     NOT NULL PRIMARY KEY, | 
				
			||||
 | 
					  grantor_uuid              TEXT     REFERENCES users (uuid), | 
				
			||||
 | 
					  grantee_uuid              TEXT     REFERENCES users (uuid), | 
				
			||||
 | 
					  email                     TEXT, | 
				
			||||
 | 
					  key_encrypted             TEXT, | 
				
			||||
 | 
					  atype                     INTEGER  NOT NULL, | 
				
			||||
 | 
					  status                    INTEGER  NOT NULL, | 
				
			||||
 | 
					  wait_time_days            INTEGER  NOT NULL, | 
				
			||||
 | 
					  recovery_initiated_at     DATETIME, | 
				
			||||
 | 
					  last_notification_at      DATETIME, | 
				
			||||
 | 
					  updated_at                DATETIME NOT NULL, | 
				
			||||
 | 
					  created_at                DATETIME NOT NULL | 
				
			||||
 | 
					); | 
				
			||||
@ -1,24 +1,805 @@ | 
				
			|||||
 | 
					use chrono::{Duration, Utc}; | 
				
			||||
use rocket::Route; | 
					use rocket::Route; | 
				
			||||
use rocket_contrib::json::Json; | 
					use rocket_contrib::json::Json; | 
				
			||||
 | 
					use serde_json::Value; | 
				
			||||
 | 
					use std::borrow::Borrow; | 
				
			||||
 | 
					
 | 
				
			||||
use crate::{api::JsonResult, auth::Headers, db::DbConn}; | 
					use crate::{ | 
				
			||||
 | 
					    api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString}, | 
				
			||||
 | 
					    auth::{decode_emergency_access_invite, Headers}, | 
				
			||||
 | 
					    db::{models::*, DbConn, DbPool}, | 
				
			||||
 | 
					    mail, CONFIG, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
pub fn routes() -> Vec<Route> { | 
					pub fn routes() -> Vec<Route> { | 
				
			||||
    routes![get_contacts,] | 
					    routes![ | 
				
			||||
 | 
					        get_contacts, | 
				
			||||
 | 
					        get_grantees, | 
				
			||||
 | 
					        get_emergency_access, | 
				
			||||
 | 
					        put_emergency_access, | 
				
			||||
 | 
					        delete_emergency_access, | 
				
			||||
 | 
					        post_delete_emergency_access, | 
				
			||||
 | 
					        send_invite, | 
				
			||||
 | 
					        resend_invite, | 
				
			||||
 | 
					        accept_invite, | 
				
			||||
 | 
					        confirm_emergency_access, | 
				
			||||
 | 
					        initiate_emergency_access, | 
				
			||||
 | 
					        approve_emergency_access, | 
				
			||||
 | 
					        reject_emergency_access, | 
				
			||||
 | 
					        takeover_emergency_access, | 
				
			||||
 | 
					        password_emergency_access, | 
				
			||||
 | 
					        view_emergency_access, | 
				
			||||
 | 
					        policies_emergency_access, | 
				
			||||
 | 
					    ] | 
				
			||||
} | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
/// This endpoint is expected to return at least something.
 | 
					// region get
 | 
				
			||||
/// If we return an error message that will trigger error toasts for the user.
 | 
					
 | 
				
			||||
/// To prevent this we just return an empty json result with no Data.
 | 
					 | 
				
			||||
/// When this feature is going to be implemented it also needs to return this empty Data
 | 
					 | 
				
			||||
/// instead of throwing an error/4XX unless it really is an error.
 | 
					 | 
				
			||||
#[get("/emergency-access/trusted")] | 
					#[get("/emergency-access/trusted")] | 
				
			||||
fn get_contacts(_headers: Headers, _conn: DbConn) -> JsonResult { | 
					fn get_contacts(headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
    debug!("Emergency access is not supported."); | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let emergency_access_list_json: Vec<Value> = | 
				
			||||
 | 
					        emergency_access_list.iter().map(|e| e.to_json_grantee_details(&conn)).collect(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					      "Data": emergency_access_list_json, | 
				
			||||
 | 
					      "Object": "list", | 
				
			||||
 | 
					      "ContinuationToken": null | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[get("/emergency-access/granted")] | 
				
			||||
 | 
					fn get_grantees(headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let emergency_access_list = EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let emergency_access_list_json: Vec<Value> = | 
				
			||||
 | 
					        emergency_access_list.iter().map(|e| e.to_json_grantor_details(&conn)).collect(); | 
				
			||||
 | 
					
 | 
				
			||||
    Ok(Json(json!({ | 
					    Ok(Json(json!({ | 
				
			||||
      "Data": [], | 
					      "Data": emergency_access_list_json, | 
				
			||||
      "Object": "list", | 
					      "Object": "list", | 
				
			||||
      "ContinuationToken": null | 
					      "ContinuationToken": null | 
				
			||||
    }))) | 
					    }))) | 
				
			||||
} | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[get("/emergency-access/<emer_id>")] | 
				
			||||
 | 
					fn get_emergency_access(emer_id: String, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emergency_access) => Ok(Json(emergency_access.to_json_grantee_details(&conn))), | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// endregion
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// region put/post
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize, Debug)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct EmergencyAccessUpdateData { | 
				
			||||
 | 
					    Type: NumberOrString, | 
				
			||||
 | 
					    WaitTimeDays: i32, | 
				
			||||
 | 
					    KeyEncrypted: Option<String>, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[put("/emergency-access/<emer_id>", data = "<data>")] | 
				
			||||
 | 
					fn put_emergency_access(emer_id: String, data: JsonUpcase<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    post_emergency_access(emer_id, data, conn) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>", data = "<data>")] | 
				
			||||
 | 
					fn post_emergency_access(emer_id: String, data: JsonUpcase<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let data: EmergencyAccessUpdateData = data.into_inner().data; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emergency_access) => emergency_access, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let new_type = match EmergencyAccessType::from_str(&data.Type.into_string()) { | 
				
			||||
 | 
					        Some(new_type) => new_type as i32, | 
				
			||||
 | 
					        None => err!("Invalid emergency access type."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    emergency_access.atype = new_type; | 
				
			||||
 | 
					    emergency_access.wait_time_days = data.WaitTimeDays; | 
				
			||||
 | 
					    emergency_access.key_encrypted = data.KeyEncrypted; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    emergency_access.save(&conn)?; | 
				
			||||
 | 
					    Ok(Json(emergency_access.to_json())) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// endregion
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// region delete
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[delete("/emergency-access/<emer_id>")] | 
				
			||||
 | 
					fn delete_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantor_user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => { | 
				
			||||
 | 
					            if emer.grantor_uuid != grantor_user.uuid && emer.grantee_uuid != Some(grantor_user.uuid) { | 
				
			||||
 | 
					                err!("Emergency access not valid.") | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            emer | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					    emergency_access.delete(&conn)?; | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/delete")] | 
				
			||||
 | 
					fn post_delete_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult { | 
				
			||||
 | 
					    delete_emergency_access(emer_id, headers, conn) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// endregion
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// region invite
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize, Debug)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct EmergencyAccessInviteData { | 
				
			||||
 | 
					    Email: String, | 
				
			||||
 | 
					    Type: NumberOrString, | 
				
			||||
 | 
					    WaitTimeDays: i32, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/invite", data = "<data>")] | 
				
			||||
 | 
					fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Headers, conn: DbConn) -> EmptyResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let data: EmergencyAccessInviteData = data.into_inner().data; | 
				
			||||
 | 
					    let email = data.Email.to_lowercase(); | 
				
			||||
 | 
					    let wait_time_days = data.WaitTimeDays; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let emergency_access_status = EmergencyAccessStatus::Invited as i32; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let new_type = match EmergencyAccessType::from_str(&data.Type.into_string()) { | 
				
			||||
 | 
					        Some(new_type) => new_type as i32, | 
				
			||||
 | 
					        None => err!("Invalid emergency access type."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantor_user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // avoid setting yourself as emergency contact
 | 
				
			||||
 | 
					    if email == grantor_user.email { | 
				
			||||
 | 
					        err!("You can not set yourself as an emergency contact.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantee_user = match User::find_by_mail(&email, &conn) { | 
				
			||||
 | 
					        None => { | 
				
			||||
 | 
					            if !CONFIG.signups_allowed() { | 
				
			||||
 | 
					                err!(format!("Grantee user does not exist: {}", email)) | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if !CONFIG.is_email_domain_allowed(&email) { | 
				
			||||
 | 
					                err!("Email domain not eligible for invitations") | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if !CONFIG.mail_enabled() { | 
				
			||||
 | 
					                let invitation = Invitation::new(email.clone()); | 
				
			||||
 | 
					                invitation.save(&conn)?; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            let mut user = User::new(email.clone()); | 
				
			||||
 | 
					            user.save(&conn)?; | 
				
			||||
 | 
					            user | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if EmergencyAccess::find_by_grantor_uuid_and_grantee_uuid_or_email( | 
				
			||||
 | 
					        &grantor_user.uuid, | 
				
			||||
 | 
					        &grantee_user.uuid, | 
				
			||||
 | 
					        &grantee_user.email, | 
				
			||||
 | 
					        &conn, | 
				
			||||
 | 
					    ) | 
				
			||||
 | 
					    .is_some() | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					        err!(format!("Grantee user already invited: {}", email)) | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let mut new_emergency_access = EmergencyAccess::new( | 
				
			||||
 | 
					        grantor_user.uuid.clone(), | 
				
			||||
 | 
					        Some(grantee_user.email.clone()), | 
				
			||||
 | 
					        emergency_access_status, | 
				
			||||
 | 
					        new_type, | 
				
			||||
 | 
					        wait_time_days, | 
				
			||||
 | 
					    ); | 
				
			||||
 | 
					    new_emergency_access.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if CONFIG.mail_enabled() { | 
				
			||||
 | 
					        mail::send_emergency_access_invite( | 
				
			||||
 | 
					            &grantee_user.email, | 
				
			||||
 | 
					            &grantee_user.uuid, | 
				
			||||
 | 
					            Some(new_emergency_access.uuid), | 
				
			||||
 | 
					            Some(grantor_user.name.clone()), | 
				
			||||
 | 
					            Some(grantor_user.email), | 
				
			||||
 | 
					        )?; | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        // Automatically mark user as accepted if no email invites
 | 
				
			||||
 | 
					        match User::find_by_mail(&email, &conn) { | 
				
			||||
 | 
					            Some(user) => { | 
				
			||||
 | 
					                match accept_invite_process(user.uuid, new_emergency_access.uuid, Some(email), conn.borrow()) { | 
				
			||||
 | 
					                    Ok(v) => (v), | 
				
			||||
 | 
					                    Err(e) => err!(e.to_string()), | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            None => err!("Grantee user not found."), | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/reinvite")] | 
				
			||||
 | 
					fn resend_invite(emer_id: String, headers: Headers, conn: DbConn) -> EmptyResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if emergency_access.grantor_uuid != headers.user.uuid { | 
				
			||||
 | 
					        err!("Emergency access not valid."); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if emergency_access.status != EmergencyAccessStatus::Invited as i32 { | 
				
			||||
 | 
					        err!("The grantee user is already accepted or confirmed to the organization"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let email = match emergency_access.email.clone() { | 
				
			||||
 | 
					        Some(email) => email, | 
				
			||||
 | 
					        None => err!("Email not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantee_user = match User::find_by_mail(&email, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Grantee user not found."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantor_user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if CONFIG.mail_enabled() { | 
				
			||||
 | 
					        mail::send_emergency_access_invite( | 
				
			||||
 | 
					            &email, | 
				
			||||
 | 
					            &grantor_user.uuid, | 
				
			||||
 | 
					            Some(emergency_access.uuid), | 
				
			||||
 | 
					            Some(grantor_user.name.clone()), | 
				
			||||
 | 
					            Some(grantor_user.email), | 
				
			||||
 | 
					        )?; | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        if Invitation::find_by_mail(&email, &conn).is_none() { | 
				
			||||
 | 
					            let invitation = Invitation::new(email); | 
				
			||||
 | 
					            invitation.save(&conn)?; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // Automatically mark user as accepted if no email invites
 | 
				
			||||
 | 
					        match accept_invite_process(grantee_user.uuid, emergency_access.uuid, emergency_access.email, conn.borrow()) { | 
				
			||||
 | 
					            Ok(v) => (v), | 
				
			||||
 | 
					            Err(e) => err!(e.to_string()), | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct AcceptData { | 
				
			||||
 | 
					    Token: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/accept", data = "<data>")] | 
				
			||||
 | 
					fn accept_invite(emer_id: String, data: JsonUpcase<AcceptData>, conn: DbConn) -> EmptyResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let data: AcceptData = data.into_inner().data; | 
				
			||||
 | 
					    let token = &data.Token; | 
				
			||||
 | 
					    let claims = decode_emergency_access_invite(token)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantee_user = match User::find_by_mail(&claims.email, &conn) { | 
				
			||||
 | 
					        Some(user) => { | 
				
			||||
 | 
					            Invitation::take(&claims.email, &conn); | 
				
			||||
 | 
					            user | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        None => err!("Invited user not found"), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // get grantor user to send Accepted email
 | 
				
			||||
 | 
					    let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Grantor user not found."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if (claims.emer_id.is_some() && emer_id == claims.emer_id.unwrap()) | 
				
			||||
 | 
					        && (claims.grantor_name.is_some() && grantor_user.name == claims.grantor_name.unwrap()) | 
				
			||||
 | 
					        && (claims.grantor_email.is_some() && grantor_user.email == claims.grantor_email.unwrap()) | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					        match accept_invite_process(grantee_user.uuid.clone(), emer_id, Some(grantee_user.email.clone()), &conn) { | 
				
			||||
 | 
					            Ok(v) => (v), | 
				
			||||
 | 
					            Err(e) => err!(e.to_string()), | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if CONFIG.mail_enabled() { | 
				
			||||
 | 
					            mail::send_emergency_access_invite_accepted(&grantor_user.email, &grantee_user.email)?; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        Ok(()) | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        err!("Emergency access invitation error.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn accept_invite_process(grantee_uuid: String, emer_id: String, email: Option<String>, conn: &DbConn) -> EmptyResult { | 
				
			||||
 | 
					    let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let emer_email = emergency_access.email; | 
				
			||||
 | 
					    if emer_email.is_none() || emer_email != email { | 
				
			||||
 | 
					        err!("User email does not match invite."); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if emergency_access.status == EmergencyAccessStatus::Accepted as i32 { | 
				
			||||
 | 
					        err!("Emergency contact already accepted."); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    emergency_access.status = EmergencyAccessStatus::Accepted as i32; | 
				
			||||
 | 
					    emergency_access.grantee_uuid = Some(grantee_uuid); | 
				
			||||
 | 
					    emergency_access.email = None; | 
				
			||||
 | 
					    emergency_access.save(conn) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct ConfirmData { | 
				
			||||
 | 
					    Key: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/confirm", data = "<data>")] | 
				
			||||
 | 
					fn confirm_emergency_access( | 
				
			||||
 | 
					    emer_id: String, | 
				
			||||
 | 
					    data: JsonUpcase<ConfirmData>, | 
				
			||||
 | 
					    headers: Headers, | 
				
			||||
 | 
					    conn: DbConn, | 
				
			||||
 | 
					) -> JsonResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let confirming_user = headers.user; | 
				
			||||
 | 
					    let data: ConfirmData = data.into_inner().data; | 
				
			||||
 | 
					    let key = data.Key; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if emergency_access.status != EmergencyAccessStatus::Accepted as i32 | 
				
			||||
 | 
					        || emergency_access.grantor_uuid != confirming_user.uuid | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					        err!("Emergency access not valid.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantor_user = match User::find_by_uuid(&confirming_user.uuid, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Grantor user not found."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { | 
				
			||||
 | 
					        let grantee_user = match User::find_by_uuid(grantee_uuid, &conn) { | 
				
			||||
 | 
					            Some(user) => user, | 
				
			||||
 | 
					            None => err!("Grantee user not found."), | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        emergency_access.status = EmergencyAccessStatus::Confirmed as i32; | 
				
			||||
 | 
					        emergency_access.key_encrypted = Some(key); | 
				
			||||
 | 
					        emergency_access.email = None; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        emergency_access.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if CONFIG.mail_enabled() { | 
				
			||||
 | 
					            mail::send_emergency_access_invite_confirmed(&grantee_user.email, &grantor_user.name)?; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        Ok(Json(emergency_access.to_json())) | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        err!("Grantee user not found.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// endregion
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// region access emergency access
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/initiate")] | 
				
			||||
 | 
					fn initiate_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let initiating_user = headers.user; | 
				
			||||
 | 
					    let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 | 
				
			||||
 | 
					        || emergency_access.grantee_uuid != Some(initiating_user.uuid.clone()) | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					        err!("Emergency access not valid.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Grantor user not found."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let now = Utc::now().naive_utc(); | 
				
			||||
 | 
					    emergency_access.status = EmergencyAccessStatus::RecoveryInitiated as i32; | 
				
			||||
 | 
					    emergency_access.updated_at = now; | 
				
			||||
 | 
					    emergency_access.recovery_initiated_at = Some(now); | 
				
			||||
 | 
					    emergency_access.last_notification_at = Some(now); | 
				
			||||
 | 
					    emergency_access.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if CONFIG.mail_enabled() { | 
				
			||||
 | 
					        mail::send_emergency_access_recovery_initiated( | 
				
			||||
 | 
					            &grantor_user.email, | 
				
			||||
 | 
					            &initiating_user.name, | 
				
			||||
 | 
					            emergency_access.get_atype_as_str(), | 
				
			||||
 | 
					            &emergency_access.wait_time_days.clone().to_string(), | 
				
			||||
 | 
					        )?; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    Ok(Json(emergency_access.to_json())) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/approve")] | 
				
			||||
 | 
					fn approve_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let approving_user = headers.user; | 
				
			||||
 | 
					    let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 | 
				
			||||
 | 
					        || emergency_access.grantor_uuid != approving_user.uuid | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					        err!("Emergency access not valid.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantor_user = match User::find_by_uuid(&approving_user.uuid, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Grantor user not found."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { | 
				
			||||
 | 
					        let grantee_user = match User::find_by_uuid(grantee_uuid, &conn) { | 
				
			||||
 | 
					            Some(user) => user, | 
				
			||||
 | 
					            None => err!("Grantee user not found."), | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32; | 
				
			||||
 | 
					        emergency_access.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if CONFIG.mail_enabled() { | 
				
			||||
 | 
					            mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name)?; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        Ok(Json(emergency_access.to_json())) | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        err!("Grantee user not found.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/reject")] | 
				
			||||
 | 
					fn reject_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let rejecting_user = headers.user; | 
				
			||||
 | 
					    let mut emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if (emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 | 
				
			||||
 | 
					        && emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32) | 
				
			||||
 | 
					        || emergency_access.grantor_uuid != rejecting_user.uuid | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					        err!("Emergency access not valid.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantor_user = match User::find_by_uuid(&rejecting_user.uuid, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Grantor user not found."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() { | 
				
			||||
 | 
					        let grantee_user = match User::find_by_uuid(grantee_uuid, &conn) { | 
				
			||||
 | 
					            Some(user) => user, | 
				
			||||
 | 
					            None => err!("Grantee user not found."), | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        emergency_access.status = EmergencyAccessStatus::Confirmed as i32; | 
				
			||||
 | 
					        emergency_access.key_encrypted = None; | 
				
			||||
 | 
					        emergency_access.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if CONFIG.mail_enabled() { | 
				
			||||
 | 
					            mail::send_emergency_access_recovery_rejected(&grantee_user.email, &grantor_user.name)?; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        Ok(Json(emergency_access.to_json())) | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        err!("Grantee user not found.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// endregion
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// region action
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/view")] | 
				
			||||
 | 
					fn view_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let requesting_user = headers.user; | 
				
			||||
 | 
					    let host = headers.host; | 
				
			||||
 | 
					    let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::View) { | 
				
			||||
 | 
					        err!("Emergency access not valid.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let ciphers = Cipher::find_owned_by_user(&emergency_access.grantor_uuid, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let ciphers_json: Vec<Value> = | 
				
			||||
 | 
					        ciphers.iter().map(|c| c.to_json(&host, &emergency_access.grantor_uuid, &conn)).collect(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					      "Ciphers": ciphers_json, | 
				
			||||
 | 
					      "KeyEncrypted": &emergency_access.key_encrypted, | 
				
			||||
 | 
					      "Object": "emergencyAccessView", | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/takeover")] | 
				
			||||
 | 
					fn takeover_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let requesting_user = headers.user; | 
				
			||||
 | 
					    let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::Takeover) { | 
				
			||||
 | 
					        err!("Emergency access not valid.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Grantor user not found."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					      "Kdf": grantor_user.client_kdf_type, | 
				
			||||
 | 
					      "KdfIterations": grantor_user.client_kdf_iter, | 
				
			||||
 | 
					      "KeyEncrypted": &emergency_access.key_encrypted, | 
				
			||||
 | 
					      "Object": "emergencyAccessTakeover", | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize, Debug)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct EmergencyAccessPasswordData { | 
				
			||||
 | 
					    NewMasterPasswordHash: String, | 
				
			||||
 | 
					    Key: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/emergency-access/<emer_id>/password", data = "<data>")] | 
				
			||||
 | 
					fn password_emergency_access( | 
				
			||||
 | 
					    emer_id: String, | 
				
			||||
 | 
					    data: JsonUpcase<EmergencyAccessPasswordData>, | 
				
			||||
 | 
					    headers: Headers, | 
				
			||||
 | 
					    conn: DbConn, | 
				
			||||
 | 
					) -> EmptyResult { | 
				
			||||
 | 
					    check_emergency_access_allowed()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let data: EmergencyAccessPasswordData = data.into_inner().data; | 
				
			||||
 | 
					    let new_master_password_hash = &data.NewMasterPasswordHash; | 
				
			||||
 | 
					    let key = data.Key; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let requesting_user = headers.user; | 
				
			||||
 | 
					    let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::Takeover) { | 
				
			||||
 | 
					        err!("Emergency access not valid.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let mut grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Grantor user not found."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // change grantor_user password
 | 
				
			||||
 | 
					    grantor_user.set_password(new_master_password_hash, None); | 
				
			||||
 | 
					    grantor_user.akey = key; | 
				
			||||
 | 
					    grantor_user.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Disable TwoFactor providers since they will otherwise block logins
 | 
				
			||||
 | 
					    TwoFactor::delete_all_by_user(&grantor_user.uuid, &conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Removing owner, check that there are at least another owner
 | 
				
			||||
 | 
					    let user_org_grantor = UserOrganization::find_any_state_by_user(&grantor_user.uuid, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Remove grantor from all organisations unless Owner
 | 
				
			||||
 | 
					    for user_org in user_org_grantor { | 
				
			||||
 | 
					        if user_org.atype != UserOrgType::Owner as i32 { | 
				
			||||
 | 
					            user_org.delete(&conn)?; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// endregion
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[get("/emergency-access/<emer_id>/policies")] | 
				
			||||
 | 
					fn policies_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let requesting_user = headers.user; | 
				
			||||
 | 
					    let emergency_access = match EmergencyAccess::find_by_uuid(&emer_id, &conn) { | 
				
			||||
 | 
					        Some(emer) => emer, | 
				
			||||
 | 
					        None => err!("Emergency access not valid."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !is_valid_request(&emergency_access, requesting_user.uuid, EmergencyAccessType::Takeover) { | 
				
			||||
 | 
					        err!("Emergency access not valid.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let grantor_user = match User::find_by_uuid(&emergency_access.grantor_uuid, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Grantor user not found."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let policies = OrgPolicy::find_by_user(&grantor_user.uuid, &conn); | 
				
			||||
 | 
					    let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Data": policies_json, | 
				
			||||
 | 
					        "Object": "list", | 
				
			||||
 | 
					        "ContinuationToken": null | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn is_valid_request( | 
				
			||||
 | 
					    emergency_access: &EmergencyAccess, | 
				
			||||
 | 
					    requesting_user_uuid: String, | 
				
			||||
 | 
					    requested_access_type: EmergencyAccessType, | 
				
			||||
 | 
					) -> bool { | 
				
			||||
 | 
					    emergency_access.grantee_uuid == Some(requesting_user_uuid) | 
				
			||||
 | 
					        && emergency_access.status == EmergencyAccessStatus::RecoveryApproved as i32 | 
				
			||||
 | 
					        && emergency_access.atype == requested_access_type as i32 | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn check_emergency_access_allowed() -> EmptyResult { | 
				
			||||
 | 
					    if !CONFIG.emergency_access_allowed() { | 
				
			||||
 | 
					        err!("Emergency access is not allowed.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn emergency_request_timeout_job(pool: DbPool) { | 
				
			||||
 | 
					    debug!("Start emergency_request_timeout_job"); | 
				
			||||
 | 
					    if !CONFIG.emergency_access_allowed() { | 
				
			||||
 | 
					        return; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if let Ok(conn) = pool.get() { | 
				
			||||
 | 
					        let emergency_access_list = EmergencyAccess::find_all_recoveries(&conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if emergency_access_list.is_empty() { | 
				
			||||
 | 
					            debug!("No emergency request timeout to approve"); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        for mut emer in emergency_access_list { | 
				
			||||
 | 
					            if emer.recovery_initiated_at.is_some() | 
				
			||||
 | 
					                && Utc::now().naive_utc() | 
				
			||||
 | 
					                    >= emer.recovery_initiated_at.unwrap() + Duration::days(emer.wait_time_days as i64) | 
				
			||||
 | 
					            { | 
				
			||||
 | 
					                emer.status = EmergencyAccessStatus::RecoveryApproved as i32; | 
				
			||||
 | 
					                emer.save(&conn).expect("Cannot save emergency access on job"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                if CONFIG.mail_enabled() { | 
				
			||||
 | 
					                    // get grantor user to send Accepted email
 | 
				
			||||
 | 
					                    let grantor_user = User::find_by_uuid(&emer.grantor_uuid, &conn).expect("Grantor user not found."); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                    // get grantee user to send Accepted email
 | 
				
			||||
 | 
					                    let grantee_user = | 
				
			||||
 | 
					                        User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid."), &conn) | 
				
			||||
 | 
					                            .expect("Grantee user not found."); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                    mail::send_emergency_access_recovery_timed_out( | 
				
			||||
 | 
					                        &grantor_user.email, | 
				
			||||
 | 
					                        &grantee_user.name.clone(), | 
				
			||||
 | 
					                        emer.get_atype_as_str(), | 
				
			||||
 | 
					                    ) | 
				
			||||
 | 
					                    .expect("Error on sending email"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                    mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name.clone()) | 
				
			||||
 | 
					                        .expect("Error on sending email"); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        error!("Failed to get DB connection while searching emergency request timed out") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn emergency_notification_reminder_job(pool: DbPool) { | 
				
			||||
 | 
					    debug!("Start emergency_notification_reminder_job"); | 
				
			||||
 | 
					    if !CONFIG.emergency_access_allowed() { | 
				
			||||
 | 
					        return; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if let Ok(conn) = pool.get() { | 
				
			||||
 | 
					        let emergency_access_list = EmergencyAccess::find_all_recoveries(&conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if emergency_access_list.is_empty() { | 
				
			||||
 | 
					            debug!("No emergency request reminder notification to send"); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        for mut emer in emergency_access_list { | 
				
			||||
 | 
					            if (emer.recovery_initiated_at.is_some() | 
				
			||||
 | 
					                && Utc::now().naive_utc() | 
				
			||||
 | 
					                    >= emer.recovery_initiated_at.unwrap() + Duration::days((emer.wait_time_days as i64) - 1)) | 
				
			||||
 | 
					                && (emer.last_notification_at.is_none() | 
				
			||||
 | 
					                    || (emer.last_notification_at.is_some() | 
				
			||||
 | 
					                        && Utc::now().naive_utc() >= emer.last_notification_at.unwrap() + Duration::days(1))) | 
				
			||||
 | 
					            { | 
				
			||||
 | 
					                emer.save(&conn).expect("Cannot save emergency access on job"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                if CONFIG.mail_enabled() { | 
				
			||||
 | 
					                    // get grantor user to send Accepted email
 | 
				
			||||
 | 
					                    let grantor_user = User::find_by_uuid(&emer.grantor_uuid, &conn).expect("Grantor user not found."); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                    // get grantee user to send Accepted email
 | 
				
			||||
 | 
					                    let grantee_user = | 
				
			||||
 | 
					                        User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid."), &conn) | 
				
			||||
 | 
					                            .expect("Grantee user not found."); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                    mail::send_emergency_access_recovery_reminder( | 
				
			||||
 | 
					                        &grantor_user.email, | 
				
			||||
 | 
					                        &grantee_user.name.clone(), | 
				
			||||
 | 
					                        emer.get_atype_as_str(), | 
				
			||||
 | 
					                        &emer.wait_time_days.to_string(), | 
				
			||||
 | 
					                    ) | 
				
			||||
 | 
					                    .expect("Error on sending email"); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        error!("Failed to get DB connection while searching emergency notification reminder") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
				
			|||||
@ -0,0 +1,290 @@ | 
				
			|||||
 | 
					use chrono::{NaiveDateTime, Utc}; | 
				
			||||
 | 
					use serde_json::Value; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use super::User; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					db_object! { | 
				
			||||
 | 
					    #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] | 
				
			||||
 | 
					    #[table_name = "emergency_access"] | 
				
			||||
 | 
					    #[changeset_options(treat_none_as_null="true")] | 
				
			||||
 | 
					    #[belongs_to(User, foreign_key = "grantor_uuid")] | 
				
			||||
 | 
					    #[primary_key(uuid)] | 
				
			||||
 | 
					    pub struct EmergencyAccess { | 
				
			||||
 | 
					        pub uuid: String, | 
				
			||||
 | 
					        pub grantor_uuid: String, | 
				
			||||
 | 
					        pub grantee_uuid: Option<String>, | 
				
			||||
 | 
					        pub email: Option<String>, | 
				
			||||
 | 
					        pub key_encrypted: Option<String>, | 
				
			||||
 | 
					        pub atype: i32, //EmergencyAccessType
 | 
				
			||||
 | 
					        pub status: i32, //EmergencyAccessStatus
 | 
				
			||||
 | 
					        pub wait_time_days: i32, | 
				
			||||
 | 
					        pub recovery_initiated_at: Option<NaiveDateTime>, | 
				
			||||
 | 
					        pub last_notification_at: Option<NaiveDateTime>, | 
				
			||||
 | 
					        pub updated_at: NaiveDateTime, | 
				
			||||
 | 
					        pub created_at: NaiveDateTime, | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					/// Local methods
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl EmergencyAccess { | 
				
			||||
 | 
					    pub fn new(grantor_uuid: String, email: Option<String>, status: i32, atype: i32, wait_time_days: i32) -> Self { | 
				
			||||
 | 
					        let now = Utc::now().naive_utc(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        Self { | 
				
			||||
 | 
					            uuid: crate::util::get_uuid(), | 
				
			||||
 | 
					            grantor_uuid, | 
				
			||||
 | 
					            grantee_uuid: None, | 
				
			||||
 | 
					            email, | 
				
			||||
 | 
					            status, | 
				
			||||
 | 
					            atype, | 
				
			||||
 | 
					            wait_time_days, | 
				
			||||
 | 
					            recovery_initiated_at: None, | 
				
			||||
 | 
					            created_at: now, | 
				
			||||
 | 
					            updated_at: now, | 
				
			||||
 | 
					            key_encrypted: None, | 
				
			||||
 | 
					            last_notification_at: None, | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn get_atype_as_str(&self) -> &'static str { | 
				
			||||
 | 
					        if self.atype == EmergencyAccessType::View as i32 { | 
				
			||||
 | 
					            "View" | 
				
			||||
 | 
					        } else { | 
				
			||||
 | 
					            "Takeover" | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn to_json(&self) -> Value { | 
				
			||||
 | 
					        json!({ | 
				
			||||
 | 
					            "Id": self.uuid, | 
				
			||||
 | 
					            "Status": self.status, | 
				
			||||
 | 
					            "Type": self.atype, | 
				
			||||
 | 
					            "WaitTimeDays": self.wait_time_days, | 
				
			||||
 | 
					            "Object": "emergencyAccess", | 
				
			||||
 | 
					        }) | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn to_json_grantor_details(&self, conn: &DbConn) -> Value { | 
				
			||||
 | 
					        // find grantor
 | 
				
			||||
 | 
					        let grantor_user = User::find_by_uuid(&self.grantor_uuid, conn).unwrap(); | 
				
			||||
 | 
					        json!({ | 
				
			||||
 | 
					             "Id": self.uuid, | 
				
			||||
 | 
					            "Status": self.status, | 
				
			||||
 | 
					            "Type": self.atype, | 
				
			||||
 | 
					            "WaitTimeDays": self.wait_time_days, | 
				
			||||
 | 
					            "GrantorId": grantor_user.uuid, | 
				
			||||
 | 
					            "Email": grantor_user.email, | 
				
			||||
 | 
					            "Name": grantor_user.name, | 
				
			||||
 | 
					            "Object": "emergencyAccessGrantorDetails",}) | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn to_json_grantee_details(&self, conn: &DbConn) -> Value { | 
				
			||||
 | 
					        if self.grantee_uuid.is_some() { | 
				
			||||
 | 
					            let grantee_user = | 
				
			||||
 | 
					                User::find_by_uuid(&self.grantee_uuid.clone().unwrap(), conn).expect("Grantee user not found."); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            json!({ | 
				
			||||
 | 
					                "Id": self.uuid, | 
				
			||||
 | 
					                "Status": self.status, | 
				
			||||
 | 
					                "Type": self.atype, | 
				
			||||
 | 
					                "WaitTimeDays": self.wait_time_days, | 
				
			||||
 | 
					                "GranteeId": grantee_user.uuid, | 
				
			||||
 | 
					                "Email": grantee_user.email, | 
				
			||||
 | 
					                "Name": grantee_user.name, | 
				
			||||
 | 
					                "Object": "emergencyAccessGranteeDetails",}) | 
				
			||||
 | 
					        } else if self.email.is_some() { | 
				
			||||
 | 
					            let grantee_user = User::find_by_mail(&self.email.clone().unwrap(), conn).expect("Grantee user not found."); | 
				
			||||
 | 
					            json!({ | 
				
			||||
 | 
					                    "Id": self.uuid, | 
				
			||||
 | 
					                    "Status": self.status, | 
				
			||||
 | 
					                    "Type": self.atype, | 
				
			||||
 | 
					                    "WaitTimeDays": self.wait_time_days, | 
				
			||||
 | 
					                    "GranteeId": grantee_user.uuid, | 
				
			||||
 | 
					                    "Email": grantee_user.email, | 
				
			||||
 | 
					                    "Name": grantee_user.name, | 
				
			||||
 | 
					                    "Object": "emergencyAccessGranteeDetails",}) | 
				
			||||
 | 
					        } else { | 
				
			||||
 | 
					            json!({ | 
				
			||||
 | 
					                "Id": self.uuid, | 
				
			||||
 | 
					                "Status": self.status, | 
				
			||||
 | 
					                "Type": self.atype, | 
				
			||||
 | 
					                "WaitTimeDays": self.wait_time_days, | 
				
			||||
 | 
					                "GranteeId": "", | 
				
			||||
 | 
					                "Email": "", | 
				
			||||
 | 
					                "Name": "", | 
				
			||||
 | 
					                "Object": "emergencyAccessGranteeDetails",}) | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] | 
				
			||||
 | 
					pub enum EmergencyAccessType { | 
				
			||||
 | 
					    View = 0, | 
				
			||||
 | 
					    Takeover = 1, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl EmergencyAccessType { | 
				
			||||
 | 
					    pub fn from_str(s: &str) -> Option<Self> { | 
				
			||||
 | 
					        match s { | 
				
			||||
 | 
					            "0" | "View" => Some(EmergencyAccessType::View), | 
				
			||||
 | 
					            "1" | "Takeover" => Some(EmergencyAccessType::Takeover), | 
				
			||||
 | 
					            _ => None, | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl PartialEq<i32> for EmergencyAccessType { | 
				
			||||
 | 
					    fn eq(&self, other: &i32) -> bool { | 
				
			||||
 | 
					        *other == *self as i32 | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl PartialEq<EmergencyAccessType> for i32 { | 
				
			||||
 | 
					    fn eq(&self, other: &EmergencyAccessType) -> bool { | 
				
			||||
 | 
					        *self == *other as i32 | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub enum EmergencyAccessStatus { | 
				
			||||
 | 
					    Invited = 0, | 
				
			||||
 | 
					    Accepted = 1, | 
				
			||||
 | 
					    Confirmed = 2, | 
				
			||||
 | 
					    RecoveryInitiated = 3, | 
				
			||||
 | 
					    RecoveryApproved = 4, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// region Database methods
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use crate::db::DbConn; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use crate::api::EmptyResult; | 
				
			||||
 | 
					use crate::error::MapResult; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl EmergencyAccess { | 
				
			||||
 | 
					    pub fn save(&mut self, conn: &DbConn) -> EmptyResult { | 
				
			||||
 | 
					        User::update_uuid_revision(&self.grantor_uuid, conn); | 
				
			||||
 | 
					        self.updated_at = Utc::now().naive_utc(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        db_run! { conn: | 
				
			||||
 | 
					            sqlite, mysql { | 
				
			||||
 | 
					                match diesel::replace_into(emergency_access::table) | 
				
			||||
 | 
					                    .values(EmergencyAccessDb::to_db(self)) | 
				
			||||
 | 
					                    .execute(conn) | 
				
			||||
 | 
					                { | 
				
			||||
 | 
					                    Ok(_) => Ok(()), | 
				
			||||
 | 
					                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
 | 
				
			||||
 | 
					                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { | 
				
			||||
 | 
					                        diesel::update(emergency_access::table) | 
				
			||||
 | 
					                            .filter(emergency_access::uuid.eq(&self.uuid)) | 
				
			||||
 | 
					                            .set(EmergencyAccessDb::to_db(self)) | 
				
			||||
 | 
					                            .execute(conn) | 
				
			||||
 | 
					                            .map_res("Error updating emergency access") | 
				
			||||
 | 
					                    } | 
				
			||||
 | 
					                    Err(e) => Err(e.into()), | 
				
			||||
 | 
					                }.map_res("Error saving emergency access") | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            postgresql { | 
				
			||||
 | 
					                let value = EmergencyAccessDb::to_db(self); | 
				
			||||
 | 
					                diesel::insert_into(emergency_access::table) | 
				
			||||
 | 
					                    .values(&value) | 
				
			||||
 | 
					                    .on_conflict(emergency_access::uuid) | 
				
			||||
 | 
					                    .do_update() | 
				
			||||
 | 
					                    .set(&value) | 
				
			||||
 | 
					                    .execute(conn) | 
				
			||||
 | 
					                    .map_res("Error saving emergency access") | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { | 
				
			||||
 | 
					        for user_org in Self::find_all_by_grantor_uuid(user_uuid, conn) { | 
				
			||||
 | 
					            user_org.delete(conn)?; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        for user_org in Self::find_all_by_grantee_uuid(user_uuid, conn) { | 
				
			||||
 | 
					            user_org.delete(conn)?; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        Ok(()) | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn delete(self, conn: &DbConn) -> EmptyResult { | 
				
			||||
 | 
					        User::update_uuid_revision(&self.grantor_uuid, conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        db_run! { conn: { | 
				
			||||
 | 
					            diesel::delete(emergency_access::table.filter(emergency_access::uuid.eq(self.uuid))) | 
				
			||||
 | 
					                .execute(conn) | 
				
			||||
 | 
					                .map_res("Error removing user from organization") | 
				
			||||
 | 
					        }} | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { | 
				
			||||
 | 
					        db_run! { conn: { | 
				
			||||
 | 
					            emergency_access::table | 
				
			||||
 | 
					                .filter(emergency_access::uuid.eq(uuid)) | 
				
			||||
 | 
					                .first::<EmergencyAccessDb>(conn) | 
				
			||||
 | 
					                .ok().from_db() | 
				
			||||
 | 
					        }} | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn find_by_grantor_uuid_and_grantee_uuid_or_email( | 
				
			||||
 | 
					        grantor_uuid: &str, | 
				
			||||
 | 
					        grantee_uuid: &str, | 
				
			||||
 | 
					        email: &str, | 
				
			||||
 | 
					        conn: &DbConn, | 
				
			||||
 | 
					    ) -> Option<Self> { | 
				
			||||
 | 
					        db_run! { conn: { | 
				
			||||
 | 
					            emergency_access::table | 
				
			||||
 | 
					                .filter(emergency_access::grantor_uuid.eq(grantor_uuid)) | 
				
			||||
 | 
					                .filter(emergency_access::grantee_uuid.eq(grantee_uuid).or(emergency_access::email.eq(email))) | 
				
			||||
 | 
					                .first::<EmergencyAccessDb>(conn) | 
				
			||||
 | 
					                .ok().from_db() | 
				
			||||
 | 
					        }} | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn find_all_recoveries(conn: &DbConn) -> Vec<Self> { | 
				
			||||
 | 
					        db_run! { conn: { | 
				
			||||
 | 
					            emergency_access::table | 
				
			||||
 | 
					                .filter(emergency_access::status.eq(EmergencyAccessStatus::RecoveryInitiated as i32)) | 
				
			||||
 | 
					                .load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db() | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        }} | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn find_by_uuid_and_grantor_uuid(uuid: &str, grantor_uuid: &str, conn: &DbConn) -> Option<Self> { | 
				
			||||
 | 
					        db_run! { conn: { | 
				
			||||
 | 
					            emergency_access::table | 
				
			||||
 | 
					                .filter(emergency_access::uuid.eq(uuid)) | 
				
			||||
 | 
					                .filter(emergency_access::grantor_uuid.eq(grantor_uuid)) | 
				
			||||
 | 
					                .first::<EmergencyAccessDb>(conn) | 
				
			||||
 | 
					                .ok().from_db() | 
				
			||||
 | 
					        }} | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn find_all_by_grantee_uuid(grantee_uuid: &str, conn: &DbConn) -> Vec<Self> { | 
				
			||||
 | 
					        db_run! { conn: { | 
				
			||||
 | 
					            emergency_access::table | 
				
			||||
 | 
					                .filter(emergency_access::grantee_uuid.eq(grantee_uuid)) | 
				
			||||
 | 
					                .load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db() | 
				
			||||
 | 
					        }} | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn find_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Option<Self> { | 
				
			||||
 | 
					        db_run! { conn: { | 
				
			||||
 | 
					            emergency_access::table | 
				
			||||
 | 
					                .filter(emergency_access::email.eq(grantee_email)) | 
				
			||||
 | 
					                .filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32)) | 
				
			||||
 | 
					                .first::<EmergencyAccessDb>(conn) | 
				
			||||
 | 
					                .ok().from_db() | 
				
			||||
 | 
					        }} | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn find_all_by_grantor_uuid(grantor_uuid: &str, conn: &DbConn) -> Vec<Self> { | 
				
			||||
 | 
					        db_run! { conn: { | 
				
			||||
 | 
					            emergency_access::table | 
				
			||||
 | 
					                .filter(emergency_access::grantor_uuid.eq(grantor_uuid)) | 
				
			||||
 | 
					                .load::<EmergencyAccessDb>(conn).expect("Error loading emergency_access").from_db() | 
				
			||||
 | 
					        }} | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// endregion
 | 
				
			||||
@ -0,0 +1,8 @@ | 
				
			|||||
 | 
					Emergency contact {{{grantee_email}}} accepted | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					This email is to notify you that {{grantee_email}} has accepted your invitation to become an emergency access contact. | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					To confirm this user, Log into {{url}} the Bitwarden web vault, go to settings and confirm the user. | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					If you do not wish to confirm this user, you can also remove them on the same page. | 
				
			||||
 | 
					{{> email/email_footer_text }} | 
				
			||||
@ -0,0 +1,21 @@ | 
				
			|||||
 | 
					Emergency contact {{{grantee_email}}} accepted | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{> email/email_header }} | 
				
			||||
 | 
					<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					        <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					            This email is to notify you that {{grantee_email}} has accepted your invitation to become an emergency access contact. | 
				
			||||
 | 
					        </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					        <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					            To confirm this user, <a href="{{url}}/">log into</a> the vaultwarden web vault, go to settings and confirm the user. | 
				
			||||
 | 
					        </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					        <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					            If you do not wish to confirm this user, you can also remove them on the same page. | 
				
			||||
 | 
					        </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					</table> | 
				
			||||
 | 
					{{> email/email_footer }} | 
				
			||||
@ -0,0 +1,6 @@ | 
				
			|||||
 | 
					Emergency contact for {{{grantor_name}}} confirmed | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					This email is to notify you that you have been confirmed as an emergency access contact for *{{grantor_name}}* was confirmed. | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					You can now initiate emergency access requests from the web vault. Log in {{url}}. | 
				
			||||
 | 
					{{> email/email_footer_text }} | 
				
			||||
@ -0,0 +1,17 @@ | 
				
			|||||
 | 
					Emergency contact for {{{grantor_name}}} confirmed | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{> email/email_header }} | 
				
			||||
 | 
					 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					           This email is to notify you that you have been confirmed as an emergency access contact for <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantor_name}}</b> was confirmed. | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					           You can now initiate emergency access requests from the web vault. <br> | 
				
			||||
 | 
					          <a href="{{url}}/">Log in</a> | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					 </table> | 
				
			||||
 | 
					{{> email/email_footer }} | 
				
			||||
@ -0,0 +1,4 @@ | 
				
			|||||
 | 
					Emergency contact request for {{{grantor_name}}} approved | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{grantor_name}} has approved your emergency request. You may now login {{url}} on the web vault and access their account. | 
				
			||||
 | 
					{{> email/email_footer_text }} | 
				
			||||
@ -0,0 +1,11 @@ | 
				
			|||||
 | 
					Emergency contact for {{{grantor_name}}} approved | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{> email/email_header }} | 
				
			||||
 | 
					 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					           <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantor_name}}</b> has approved your emergency request. You may now <a href="{{url}}/">login</a> on the web vault and access their account. | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					 </table> | 
				
			||||
 | 
					{{> email/email_footer }} | 
				
			||||
@ -0,0 +1,6 @@ | 
				
			|||||
 | 
					Emergency access request by {{{grantee_name}}} initiated | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{grantee_name}} has initiated an emergency request to *{{atype}}* your account. You may login on the web vault and manually approve or reject this request. | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					If you do nothing, the request will automatically be approved after {{wait_time_days}} day(s). | 
				
			||||
 | 
					{{> email/email_footer_text }} | 
				
			||||
@ -0,0 +1,16 @@ | 
				
			|||||
 | 
					Emergency access request by {{{grantee_name}}} initiated | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{> email/email_header }} | 
				
			||||
 | 
					 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					           <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantee_name}}</b> has initiated an emergency request to <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{atype}}</b> your account. You may login on the web vault and manually approve or reject this request. | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					           If you do nothing, the request will automatically be approved after <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{wait_time_days}}</b> day(s). | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					 </table> | 
				
			||||
 | 
					{{> email/email_footer }} | 
				
			||||
@ -0,0 +1,4 @@ | 
				
			|||||
 | 
					Emergency access request to {{{grantor_name}}} rejected | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{grantor_name}} has rejected your emergency request. | 
				
			||||
 | 
					{{> email/email_footer_text }} | 
				
			||||
@ -0,0 +1,11 @@ | 
				
			|||||
 | 
					Emergency access request to {{{grantor_name}}} rejected | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{> email/email_header }} | 
				
			||||
 | 
					 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					           <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantor_name}}</b>  has rejected your emergency request. | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					 </table> | 
				
			||||
 | 
					{{> email/email_footer }} | 
				
			||||
@ -0,0 +1,6 @@ | 
				
			|||||
 | 
					Emergency access request by {{{grantee_name}}} is pending | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{grantee_name}} has a pending emergency request to *{{atype}}* your account. You may login on the web vault and manually approve or reject this request. | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					If you do nothing, the request will automatically be approved after {{wait_time_days}} day(s). | 
				
			||||
 | 
					{{> email/email_footer_text }} | 
				
			||||
@ -0,0 +1,16 @@ | 
				
			|||||
 | 
					Emergency access request by {{{grantee_name}}} is pending | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{> email/email_header }} | 
				
			||||
 | 
					 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					           <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantee_name}}</b> has a pending emergency request to <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{atype}}</b> your account. You may login on the web vault and manually approve or reject this request. | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					           If you do nothing, the request will automatically be approved after <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{wait_time_days}}</b> day(s). | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					 </table> | 
				
			||||
 | 
					{{> email/email_footer }} | 
				
			||||
@ -0,0 +1,4 @@ | 
				
			|||||
 | 
					Emergency access request by {{{grantee_name}}} granted | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{grantee_name}} has been granted emergency request to *{{atype}}* your account. You may login on the web vault and manually revoke this request. | 
				
			||||
 | 
					{{> email/email_footer_text }} | 
				
			||||
@ -0,0 +1,11 @@ | 
				
			|||||
 | 
					Emergency access request by {{{grantee_name}}} granted | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{> email/email_header }} | 
				
			||||
 | 
					 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					           <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantee_name}}</b> has been granted emergency request to <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{atype}}</b> your account. You may login on the web vault and manually revoke this request. | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					 </table> | 
				
			||||
 | 
					{{> email/email_footer }} | 
				
			||||
@ -0,0 +1,8 @@ | 
				
			|||||
 | 
					Emergency access for {{{grantor_name}}} | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					You have been invited to become an emergency contact for {{grantor_name}}. To accept this invite, click the following link: | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					Click here to join: {{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email. | 
				
			||||
 | 
					{{> email/email_footer_text }} | 
				
			||||
@ -0,0 +1,24 @@ | 
				
			|||||
 | 
					Emergency access for {{{grantor_name}}} | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					{{> email/email_header }} | 
				
			||||
 | 
					 <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> | 
				
			||||
 | 
					          You have been invited to become an emergency contact for <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{grantor_name}}</b>. | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> | 
				
			||||
 | 
					          <a href="{{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}" | 
				
			||||
 | 
					             clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					          Become emergency contact | 
				
			||||
 | 
					          </a> | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> | 
				
			||||
 | 
					           If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email. | 
				
			||||
 | 
					       </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					 </table> | 
				
			||||
 | 
					{{> email/email_footer }} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue